diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS
index a8db269c..7695e96f 100644
--- a/.github/APPROVED_CONTRIBUTORS
+++ b/.github/APPROVED_CONTRIBUTORS
@@ -241,3 +241,5 @@ dangooddd pr
Mearman pr
dodiego pr
+
+any-victor pr
diff --git a/.github/ISSUE_TEMPLATE/package-report.yml b/.github/ISSUE_TEMPLATE/package-report.yml
new file mode 100644
index 00000000..846e25ee
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/package-report.yml
@@ -0,0 +1,49 @@
+name: Package Report
+description: Report a problematic Pi package listed on pi.dev
+labels: ["package-report"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Use this form to report a package listed on pi.dev. For Pi core bugs, use the bug report template instead.
+
+ New issues from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/earendil-works/pi-mono/blob/main/CONTRIBUTING.md) will not be reopened or receive a reply.
+
+ Keep this short. If it doesn't fit on one screen, it's too long. Write in your own voice.
+
+ - type: input
+ id: package-name
+ attributes:
+ label: Package name
+ description: The npm package name from pi.dev.
+ placeholder: "@scope/package"
+ validations:
+ required: true
+
+ - type: input
+ id: package-version
+ attributes:
+ label: Version
+ description: The package version shown on pi.dev.
+ placeholder: "0.1.0"
+ validations:
+ required: false
+
+ - type: dropdown
+ id: report-type
+ attributes:
+ label: What are you reporting?
+ options:
+ - Malicious or unsafe behavior
+ - Impersonation
+ - Trademark / TOS Violations
+ validations:
+ required: true
+
+ - type: textarea
+ id: details
+ attributes:
+ label: Details
+ description: Describe the concern and include links, logs, or screenshots if helpful.
+ validations:
+ required: true
diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml
index 355ad1cd..dbeb6e53 100644
--- a/.github/workflows/build-binaries.yml
+++ b/.github/workflows/build-binaries.yml
@@ -17,11 +17,17 @@ on:
permissions: {}
+concurrency:
+ group: build-binaries-${{ github.event.inputs.tag || github.ref_name }}
+ cancel-in-progress: false
+
jobs:
+ # Keep the public GitHub Release publication last. Binary assets are staged in
+ # a draft release first; cleanup removes the draft if later publishing fails.
build:
runs-on: ubuntu-latest
permissions:
- contents: write
+ contents: read
env:
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
SOURCE_REF: ${{ github.event.inputs.source_ref || github.event.inputs.tag || github.ref_name }}
@@ -46,17 +52,16 @@ jobs:
- name: Build binaries
run: ./scripts/build-binaries.sh
- - name: Extract changelog for this version
- id: changelog
+ - name: Prepare GitHub release payload
run: |
+ set -euo pipefail
+
+ mkdir -p release-assets
+
VERSION="${RELEASE_TAG}"
VERSION="${VERSION#v}" # Remove 'v' prefix
- node scripts/release-notes.mjs extract --version "${VERSION}" --tag "${RELEASE_TAG}" --out /tmp/release-notes.md
+ node scripts/release-notes.mjs extract --version "${VERSION}" --tag "${RELEASE_TAG}" --out release-assets/RELEASE_NOTES.md
- - name: Create GitHub Release and upload binaries
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
cd packages/coding-agent/binaries
release_assets=(
@@ -67,24 +72,107 @@ jobs:
pi-windows-x64.zip
pi-windows-arm64.zip
)
- sha256sum "${release_assets[@]}" > SHA256SUMS
- release_assets+=(SHA256SUMS)
- if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
- gh release edit "${RELEASE_TAG}" \
- --title "${RELEASE_TAG}" \
- --notes-file /tmp/release-notes.md
- gh release upload "${RELEASE_TAG}" "${release_assets[@]}" --clobber
- else
- gh release create "${RELEASE_TAG}" \
- --title "${RELEASE_TAG}" \
- --notes-file /tmp/release-notes.md \
- "${release_assets[@]}"
+ for asset in "${release_assets[@]}"; do
+ test -f "${asset}"
+ done
+
+ sha256sum "${release_assets[@]}" > "${GITHUB_WORKSPACE}/release-assets/SHA256SUMS"
+ cp "${release_assets[@]}" "${GITHUB_WORKSPACE}/release-assets/"
+
+ - name: Upload GitHub release payload
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: release-assets-${{ env.RELEASE_TAG }}
+ path: release-assets/*
+ if-no-files-found: error
+ retention-days: 14
+
+ stage-github-release:
+ runs-on: ubuntu-latest
+ needs: build
+ permissions:
+ actions: read
+ contents: write
+ env:
+ GH_REPO: ${{ github.repository }}
+ RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
+ steps:
+ - name: Download GitHub release payload
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: release-assets-${{ env.RELEASE_TAG }}
+ path: release-assets
+
+ - name: Validate GitHub release payload
+ run: |
+ set -euo pipefail
+
+ cd release-assets
+
+ expected_assets=(
+ pi-darwin-arm64.tar.gz
+ pi-darwin-x64.tar.gz
+ pi-linux-x64.tar.gz
+ pi-linux-arm64.tar.gz
+ pi-windows-x64.zip
+ pi-windows-arm64.zip
+ SHA256SUMS
+ RELEASE_NOTES.md
+ )
+
+ for asset in "${expected_assets[@]}"; do
+ test -f "${asset}"
+ done
+
+ sha256sum -c SHA256SUMS
+
+ - name: Create draft GitHub Release and upload binaries
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ set -euo pipefail
+
+ cd release-assets
+
+ release_assets=(
+ pi-darwin-arm64.tar.gz
+ pi-darwin-x64.tar.gz
+ pi-linux-x64.tar.gz
+ pi-linux-arm64.tar.gz
+ pi-windows-x64.zip
+ pi-windows-arm64.zip
+ SHA256SUMS
+ )
+
+ existing_release="$(gh release view "${RELEASE_TAG}" --json isDraft --jq .isDraft 2>/dev/null || true)"
+ if [[ "${existing_release}" == "false" ]]; then
+ echo "::error::GitHub Release ${RELEASE_TAG} is already published. Refusing to mutate a public release."
+ exit 1
+ fi
+ if [[ "${existing_release}" == "true" ]]; then
+ gh release delete "${RELEASE_TAG}" --yes
+ fi
+
+ gh release create "${RELEASE_TAG}" \
+ --verify-tag \
+ --draft \
+ --title "${RELEASE_TAG}" \
+ --notes-file RELEASE_NOTES.md \
+ "${release_assets[@]}"
+
+ expected_asset_names="$(printf '%s\n' "${release_assets[@]}" | sort)"
+ actual_asset_names="$(gh release view "${RELEASE_TAG}" --json assets --jq '.assets[].name' | sort)"
+
+ if [[ "${actual_asset_names}" != "${expected_asset_names}" ]]; then
+ echo "::error::Draft GitHub Release asset set does not match expected files."
+ diff -u <(printf '%s\n' "${expected_asset_names}") <(printf '%s\n' "${actual_asset_names}") || true
+ exit 1
fi
publish-npm:
runs-on: ubuntu-latest
- needs: build
+ needs: stage-github-release
environment: npm-publish
permissions:
contents: read
@@ -124,9 +212,6 @@ jobs:
- name: Test
run: npm test
- - name: Verify release artifacts are committed
- run: git diff --exit-code
-
- name: Upgrade npm for trusted publishing
run: |
npm install -g npm@11.16.0 --ignore-scripts
@@ -134,3 +219,57 @@ jobs:
- name: Publish npm packages
run: node scripts/publish.mjs
+
+ publish-github-release:
+ runs-on: ubuntu-latest
+ needs:
+ - stage-github-release
+ - publish-npm
+ permissions:
+ contents: write
+ env:
+ GH_REPO: ${{ github.repository }}
+ RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
+ steps:
+ - name: Publish staged GitHub Release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ set -euo pipefail
+
+ existing_release="$(gh release view "${RELEASE_TAG}" --json isDraft --jq .isDraft 2>/dev/null || true)"
+ if [[ "${existing_release}" == "" ]]; then
+ echo "::error::Draft GitHub Release ${RELEASE_TAG} does not exist."
+ exit 1
+ fi
+ if [[ "${existing_release}" == "false" ]]; then
+ echo "::error::GitHub Release ${RELEASE_TAG} is already published."
+ exit 1
+ fi
+
+ gh release edit "${RELEASE_TAG}" --draft=false
+
+ cleanup-draft-github-release:
+ runs-on: ubuntu-latest
+ needs:
+ - build
+ - stage-github-release
+ - publish-npm
+ - publish-github-release
+ if: ${{ always() && needs.stage-github-release.result != 'skipped' && (needs.stage-github-release.result != 'success' || needs.publish-npm.result != 'success' || needs.publish-github-release.result != 'success') }}
+ permissions:
+ contents: write
+ env:
+ GH_REPO: ${{ github.repository }}
+ RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
+ steps:
+ - name: Delete draft GitHub Release after failure
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ set -euo pipefail
+
+ existing_release="$(gh release view "${RELEASE_TAG}" --json isDraft --jq .isDraft 2>/dev/null || true)"
+ if [[ "${existing_release}" == "true" ]]; then
+ gh release delete "${RELEASE_TAG}" --yes
+ fi
diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml
index 62663940..d2bf830e 100644
--- a/.github/workflows/issue-gate.yml
+++ b/.github/workflows/issue-gate.yml
@@ -111,9 +111,17 @@ jobs:
body: message,
});
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ labels: ['untriaged'],
+ });
+
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed',
+ state_reason: 'not_planned',
});
diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml
new file mode 100644
index 00000000..0e6204d5
--- /dev/null
+++ b/.github/workflows/issue-triage-labels.yml
@@ -0,0 +1,142 @@
+name: Issue Triage Labels
+
+on:
+ issues:
+ types: [reopened, labeled]
+
+jobs:
+ update-labels:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ steps:
+ - name: Update triage labels
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const UNTRIAGED_LABEL = 'untriaged';
+ const NO_ACTION_LABEL = 'no-action';
+ const LAST_READ_LABEL = 'last-read';
+ const TO_DISCUSS_LABEL = 'to-discuss';
+ const INPROGRESS_LABEL = 'inprogress';
+
+ function issueHasLabel(issue, labelName) {
+ return (issue.labels ?? []).some((label) => label.name === labelName);
+ }
+
+ async function removeLabelIfPresent(issueNumber, issue, labelName) {
+ if (!issueHasLabel(issue, labelName)) {
+ console.log(`Issue #${issueNumber} does not have ${labelName}`);
+ return;
+ }
+
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ name: labelName,
+ });
+ console.log(`Removed ${labelName} from #${issueNumber}`);
+ } catch (error) {
+ if (error.status === 404) {
+ console.log(`Label ${labelName} was already absent from #${issueNumber}`);
+ return;
+ }
+ throw error;
+ }
+ }
+
+ if (context.payload.action === 'reopened') {
+ await removeLabelIfPresent(context.issue.number, context.payload.issue, UNTRIAGED_LABEL);
+ await removeLabelIfPresent(context.issue.number, context.payload.issue, NO_ACTION_LABEL);
+ return;
+ }
+
+ if (context.payload.action === 'labeled' && context.payload.label?.name === NO_ACTION_LABEL) {
+ await removeLabelIfPresent(context.issue.number, context.payload.issue, UNTRIAGED_LABEL);
+ return;
+ }
+
+ if (context.payload.action !== 'labeled' || context.payload.label?.name !== LAST_READ_LABEL) {
+ console.log('Not a last-read label event');
+ return;
+ }
+
+ const currentIssueNumber = context.issue.number;
+ const lastReadIssues = await github.paginate(github.rest.issues.listForRepo, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'all',
+ labels: LAST_READ_LABEL,
+ per_page: 100,
+ });
+
+ const previousIssueNumbers = lastReadIssues
+ .filter((issue) => !issue.pull_request)
+ .map((issue) => issue.number)
+ .filter((issueNumber) => issueNumber !== currentIssueNumber);
+
+ if (previousIssueNumbers.length === 0) {
+ console.log('No previous last-read issue found');
+ return;
+ }
+
+ const previousIssueNumber = Math.max(...previousIssueNumbers);
+ if (currentIssueNumber <= previousIssueNumber) {
+ console.log(
+ `Last-read was added to old issue #${currentIssueNumber}; latest last-read is #${previousIssueNumber}`,
+ );
+ return;
+ }
+
+ const untriagedIssues = await github.paginate(github.rest.issues.listForRepo, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'all',
+ labels: UNTRIAGED_LABEL,
+ per_page: 100,
+ });
+
+ const issuesToMark = untriagedIssues
+ .filter((issue) => !issue.pull_request)
+ .filter((issue) => issue.number >= previousIssueNumber && issue.number <= currentIssueNumber)
+ .sort((a, b) => a.number - b.number);
+
+ if (issuesToMark.length === 0) {
+ console.log(`No untriaged issues found from #${previousIssueNumber} to #${currentIssueNumber}`);
+ return;
+ }
+
+ for (const issue of issuesToMark) {
+ if (issueHasLabel(issue, TO_DISCUSS_LABEL)) {
+ console.log(`Skipped ${NO_ACTION_LABEL} for #${issue.number} because it has ${TO_DISCUSS_LABEL}`);
+ } else {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ labels: [NO_ACTION_LABEL],
+ });
+ console.log(`Added ${NO_ACTION_LABEL} to #${issue.number}`);
+ }
+
+ await github.rest.issues.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ state: 'closed',
+ state_reason: 'not_planned',
+ });
+ console.log(`Closed #${issue.number} as not planned`);
+
+ await removeLabelIfPresent(issue.number, issue, INPROGRESS_LABEL);
+
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ name: UNTRIAGED_LABEL,
+ });
+ console.log(`Removed ${UNTRIAGED_LABEL} from #${issue.number}`);
+ }
diff --git a/.github/workflows/openclaw-gate.yml b/.github/workflows/openclaw-gate.yml
deleted file mode 100644
index 126d4ebf..00000000
--- a/.github/workflows/openclaw-gate.yml
+++ /dev/null
@@ -1,122 +0,0 @@
-name: OpenClaw Gate
-
-on:
- issues:
- types: [opened]
- pull_request_target:
- types: [opened]
-
-jobs:
- check-contributor:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- issues: write
- pull-requests: write
- steps:
- - name: Check contributor
- uses: actions/github-script@v7
- with:
- script: |
- const isPR = !!context.payload.pull_request;
- const author = isPR
- ? context.payload.pull_request.user.login
- : context.payload.issue.user.login;
- const number = isPR
- ? context.payload.pull_request.number
- : context.payload.issue.number;
- const defaultBranch = context.payload.repository.default_branch;
-
- if (author.endsWith('[bot]') || author === 'dependabot[bot]') {
- console.log(`Skipping bot: ${author}`);
- return;
- }
-
- const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS';
- const VALID_CAPABILITIES = new Set(['issue', 'pr']);
-
- // --- Check APPROVED_CONTRIBUTORS ---
- async function getTextFile(path) {
- const { data } = await github.rest.repos.getContent({
- owner: context.repo.owner,
- repo: context.repo.repo,
- path,
- ref: defaultBranch,
- });
- if (!('content' in data) || typeof data.content !== 'string') {
- throw new Error(`Expected file content for ${path}`);
- }
- return Buffer.from(data.content, 'base64').toString('utf8');
- }
-
- try {
- const content = await getTextFile(APPROVED_FILE);
- const approved = new Map();
- for (const rawLine of content.split('\n')) {
- const line = rawLine.trim();
- if (!line || line.startsWith('#')) continue;
-
- const parts = line.split(/\s+/);
- if (parts.length !== 2) continue;
-
- const [username, capability] = parts;
- const normalizedCapability = capability.toLowerCase();
- if (!VALID_CAPABILITIES.has(normalizedCapability)) continue;
-
- approved.set(username.toLowerCase(), normalizedCapability);
- }
-
- if (approved.has(author.toLowerCase())) {
- console.log(`${author} is in APPROVED_CONTRIBUTORS, passing`);
- return;
- }
- } catch (err) {
- console.log(`Could not read APPROVED_CONTRIBUTORS: ${err.message}`);
- }
-
- // --- Also pass collaborators with write+ access ---
- try {
- const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
- owner: context.repo.owner,
- repo: context.repo.repo,
- username: author,
- });
- if (['admin', 'maintain', 'write'].includes(perm.permission)) {
- console.log(`${author} is a collaborator (${perm.permission}), passing`);
- return;
- }
- } catch {
- // not a collaborator
- }
-
- // --- Check if user opened issues/PRs on openclaw/openclaw ---
- async function hasOpenClawActivity(username) {
- try {
- const { data } = await github.rest.search.issuesAndPullRequests({
- q: `repo:openclaw/openclaw author:${username}`,
- per_page: 1,
- });
- if (data.total_count > 0) {
- console.log(`${username} has opened ${data.total_count} issues/PRs on openclaw/openclaw`);
- return true;
- }
- } catch (err) {
- console.log(`Search failed: ${err.message}`);
- }
- return false;
- }
-
- const hasActivity = await hasOpenClawActivity(author);
- if (!hasActivity) {
- console.log(`${author} has no openclaw/openclaw activity, passing`);
- return;
- }
-
- // --- Add openclaw label ---
- console.log(`${author} has openclaw/openclaw activity, adding label`);
- await github.rest.issues.addLabels({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: number,
- labels: ['possibly-openclaw-clanker'],
- });
diff --git a/.github/workflows/remove-inprogress-on-close.yml b/.github/workflows/remove-inprogress-on-close.yml
new file mode 100644
index 00000000..e94b10e2
--- /dev/null
+++ b/.github/workflows/remove-inprogress-on-close.yml
@@ -0,0 +1,31 @@
+name: Remove In Progress Label On Close
+
+on:
+ issues:
+ types: [closed]
+
+jobs:
+ remove-label:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ steps:
+ - name: Remove inprogress label
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const labelName = 'inprogress';
+ const labels = context.payload.issue.labels ?? [];
+ const hasLabel = labels.some((label) => label.name === labelName);
+
+ if (!hasLabel) {
+ console.log(`Issue does not have ${labelName} label`);
+ return;
+ }
+
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ name: labelName,
+ });
diff --git a/README.md b/README.md
index dc31b930..130a412f 100644
--- a/README.md
+++ b/README.md
@@ -5,46 +5,24 @@
-
-
- pi.dev domain graciously donated by
-
- exe.dev
+
> New issues and PRs from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. See [CONTRIBUTING.md](CONTRIBUTING.md).
----
+# Pi Agent Harness
-# Pi Agent Harness Mono Repo
-
-This is the home of the pi agent harness project including our self extensible coding agent.
+This is the home of the Pi agent harness project including our self extensible coding agent.
* **[@earendil-works/pi-coding-agent](packages/coding-agent)**: Interactive coding agent CLI
* **[@earendil-works/pi-agent-core](packages/agent)**: Agent runtime with tool calling and state management
* **[@earendil-works/pi-ai](packages/ai)**: Unified multi-provider LLM API (OpenAI, Anthropic, Google, …)
-To learn more about pi:
+To learn more about Pi:
* [Visit pi.dev](https://pi.dev), the project website with demos
* [Read the documentation](https://pi.dev/docs/latest), but you can also ask the agent to explain itself
-## Share your OSS coding agent sessions
-
-If you use pi or other coding agents for open source work, please share your sessions.
-
-Public OSS session data helps improve coding agents with real-world tasks, tool use, failures, and fixes instead of toy benchmarks.
-
-For the full explanation, see [this post on X](https://x.com/badlogicgames/status/2037811643774652911).
-
-To publish sessions, use [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Read its README.md for setup instructions. All you need is a Hugging Face account, the Hugging Face CLI, and `pi-share-hf`.
-
-You can also watch [this video](https://x.com/badlogicgames/status/2041151967695634619), where I show how I publish my `pi-mono` sessions.
-
-I regularly publish my own `pi-mono` work sessions here:
-
-- [badlogicgames/pi-mono on Hugging Face](https://huggingface.co/datasets/badlogicgames/pi-mono)
-
## All Packages
| Package | Description |
@@ -94,6 +72,28 @@ We treat npm dependency changes as reviewed code changes.
- CI installs with `npm ci --ignore-scripts`, and a scheduled GitHub workflow runs `npm audit --omit=dev` plus `npm audit signatures --omit=dev`.
- Shrinkwrap generation has an explicit allowlist for dependency lifecycle scripts; new lifecycle-script deps fail checks until reviewed.
+## Share your OSS coding agent sessions
+
+If you use Pi or other coding agents for open source work, please share your sessions.
+
+Public OSS session data helps improve coding agents with real-world tasks, tool use, failures, and fixes instead of toy benchmarks.
+
+For the full explanation, see [this post on X](https://x.com/badlogicgames/status/2037811643774652911).
+
+To publish sessions, use [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Read its README.md for setup instructions. All you need is a Hugging Face account, the Hugging Face CLI, and `pi-share-hf`.
+
+You can also watch [this video](https://x.com/badlogicgames/status/2041151967695634619), where I show how I publish my `pi-mono` sessions.
+
+I regularly publish my own `pi-mono` work sessions here:
+
+- [badlogicgames/pi-mono on Hugging Face](https://huggingface.co/datasets/badlogicgames/pi-mono)
+
## License
MIT
+
+
+ pi.dev domain graciously donated by
+
+ exe.dev
+
diff --git a/biome.json b/biome.json
index 7c89d187..b5451402 100644
--- a/biome.json
+++ b/biome.json
@@ -31,6 +31,7 @@
"!**/node_modules/**/*",
"!**/test-sessions.ts",
"!**/models.generated.ts",
+ "!**/*.models.ts",
"!packages/mom/data/**/*",
"!!**/node_modules"
]
diff --git a/package-lock.json b/package-lock.json
index c9508f4d..841495d6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,7 @@
"@biomejs/biome": "2.3.5",
"@types/node": "22.19.19",
"@typescript/native-preview": "7.0.0-dev.20260120.1",
- "esbuild": "0.28.0",
+ "esbuild": "0.28.1",
"husky": "9.1.7",
"jiti": "2.7.0",
"shx": "0.4.0",
@@ -31,20 +31,6 @@
"node": ">=22.19.0"
}
},
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@anthropic-ai/sandbox-runtime": {
"version": "0.0.26",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.26.tgz",
@@ -815,10 +801,44 @@
"resolved": "packages/tui",
"link": true
},
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
- "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"cpu": [
"ppc64"
],
@@ -833,9 +853,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
- "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"cpu": [
"arm"
],
@@ -850,9 +870,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
- "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"cpu": [
"arm64"
],
@@ -867,9 +887,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
- "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"cpu": [
"x64"
],
@@ -884,9 +904,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
- "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"cpu": [
"arm64"
],
@@ -901,9 +921,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
- "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"cpu": [
"x64"
],
@@ -918,9 +938,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
- "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"cpu": [
"arm64"
],
@@ -935,9 +955,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
- "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"cpu": [
"x64"
],
@@ -952,9 +972,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
- "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"cpu": [
"arm"
],
@@ -969,9 +989,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
- "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"cpu": [
"arm64"
],
@@ -986,9 +1006,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
- "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"cpu": [
"ia32"
],
@@ -1003,9 +1023,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
- "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"cpu": [
"loong64"
],
@@ -1020,9 +1040,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
- "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"cpu": [
"mips64el"
],
@@ -1037,9 +1057,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
- "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"cpu": [
"ppc64"
],
@@ -1054,9 +1074,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
- "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"cpu": [
"riscv64"
],
@@ -1071,9 +1091,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
- "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"cpu": [
"s390x"
],
@@ -1088,9 +1108,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
- "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"cpu": [
"x64"
],
@@ -1105,9 +1125,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
- "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
@@ -1122,9 +1142,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
- "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"cpu": [
"x64"
],
@@ -1139,9 +1159,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
- "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
@@ -1156,9 +1176,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
- "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"cpu": [
"x64"
],
@@ -1173,9 +1193,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
- "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
@@ -1190,9 +1210,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
- "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"cpu": [
"x64"
],
@@ -1207,9 +1227,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
- "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"cpu": [
"arm64"
],
@@ -1224,9 +1244,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
- "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"cpu": [
"ia32"
],
@@ -1241,9 +1261,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
- "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"cpu": [
"x64"
],
@@ -1281,130 +1301,6 @@
}
}
},
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@isaacs/cliui/node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.2.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/@istanbuljs/schema": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz",
- "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -1613,14 +1509,42 @@
}
},
"node_modules/@mistralai/mistralai": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz",
- "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==",
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz",
+ "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==",
"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/@napi-rs/wasm-runtime": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+ "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@nodable/entities": {
@@ -1673,17 +1597,34 @@
"node": ">= 8"
}
},
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "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/@oxc-project/types": {
+ "version": "0.133.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
+ "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
"node_modules/@pondwader/socks5-server": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz",
@@ -1709,9 +1650,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
- "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "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": {
@@ -1729,12 +1670,6 @@
"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",
@@ -1753,24 +1688,10 @@
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
"license": "BSD-3-Clause"
},
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
- "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
- "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
+ "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"cpu": [
"arm64"
],
@@ -1779,12 +1700,15 @@
"optional": true,
"os": [
"android"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
- "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
+ "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"cpu": [
"arm64"
],
@@ -1793,12 +1717,15 @@
"optional": true,
"os": [
"darwin"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
- "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
+ "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"cpu": [
"x64"
],
@@ -1807,26 +1734,15 @@
"optional": true,
"os": [
"darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
- "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
- "cpu": [
- "arm64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
- "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
+ "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"cpu": [
"x64"
],
@@ -1835,12 +1751,15 @@
"optional": true,
"os": [
"freebsd"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
- "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
+ "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"cpu": [
"arm"
],
@@ -1849,194 +1768,135 @@
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
- "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
- "cpu": [
- "arm"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
- "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
+ "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"cpu": [
"arm64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
- "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
+ "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"cpu": [
"arm64"
],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
- "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
- "cpu": [
- "loong64"
+ "libc": [
+ "musl"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
- "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
- "cpu": [
- "loong64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
- "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
+ "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"cpu": [
"ppc64"
],
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
- "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
- "cpu": [
- "ppc64"
+ "libc": [
+ "glibc"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
- "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
- "cpu": [
- "riscv64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
- "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
- "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
+ "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"cpu": [
"s390x"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
- "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
+ "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"cpu": [
"x64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
- "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
+ "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"cpu": [
"x64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
- "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
- "cpu": [
- "x64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
- "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
+ "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"cpu": [
"arm64"
],
@@ -2045,12 +1905,34 @@
"optional": true,
"os": [
"openharmony"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
- "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
+ "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
+ "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"cpu": [
"arm64"
],
@@ -2059,26 +1941,15 @@
"optional": true,
"os": [
"win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
- "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
- "cpu": [
- "ia32"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
- "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
+ "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [
"x64"
],
@@ -2087,21 +1958,17 @@
"optional": true,
"os": [
"win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
- "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
- "cpu": [
- "x64"
],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
+ "license": "MIT"
},
"node_modules/@silvia-odwyer/photon-node": {
"version": "0.3.4",
@@ -2229,6 +2096,24 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -2450,155 +2335,6 @@
"win32"
]
},
- "node_modules/@vitest/coverage-v8": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
- "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@ampproject/remapping": "^2.3.0",
- "@bcoe/v8-coverage": "^1.0.2",
- "ast-v8-to-istanbul": "^0.3.3",
- "debug": "^4.4.1",
- "istanbul-lib-coverage": "^3.2.2",
- "istanbul-lib-report": "^3.0.1",
- "istanbul-lib-source-maps": "^5.0.6",
- "istanbul-reports": "^3.1.7",
- "magic-string": "^0.30.17",
- "magicast": "^0.3.5",
- "std-env": "^3.9.0",
- "test-exclude": "^7.0.1",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@vitest/browser": "3.2.4",
- "vitest": "3.2.4"
- },
- "peerDependenciesMeta": {
- "@vitest/browser": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/expect": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
- "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/chai": "^5.2.2",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
- "chai": "^5.2.0",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/mocker": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
- "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "3.2.4",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.17"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/pretty-format": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
- "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
- "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "3.2.4",
- "pathe": "^2.0.3",
- "strip-literal": "^3.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/snapshot": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
- "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "3.2.4",
- "magic-string": "^0.30.17",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/spy": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
- "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyspy": "^4.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
- "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "3.2.4",
- "loupe": "^3.1.4",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
"node_modules/@xterm/headless": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz",
@@ -2615,32 +2351,6 @@
"node": ">= 14"
}
},
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
@@ -2660,18 +2370,6 @@
"node": ">=12"
}
},
- "node_modules/ast-v8-to-istanbul": {
- "version": "0.3.12",
- "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz",
- "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.31",
- "estree-walker": "^3.0.3",
- "js-tokens": "^10.0.0"
- }
- },
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -2802,16 +2500,6 @@
"node": ">=10.0.0"
}
},
- "node_modules/cac": {
- "version": "6.7.14",
- "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/canvas": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz",
@@ -2839,23 +2527,6 @@
"node": ">=20"
}
},
- "node_modules/chai": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
@@ -2868,16 +2539,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/check-error": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
- "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- }
- },
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
@@ -2885,26 +2546,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -2914,6 +2555,13 @@
"node": ">=18"
}
},
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/cpu-features": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
@@ -2984,16 +2632,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/deep-eql": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -3023,13 +2661,6 @@
"node": ">=0.3.1"
}
},
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -3039,13 +2670,6 @@
"safe-buffer": "^5.0.1"
}
},
- "node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -3066,17 +2690,10 @@
"node": ">= 0.4"
}
},
- "node_modules/es-module-lexer": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/esbuild": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
- "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -3087,32 +2704,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.0",
- "@esbuild/android-arm": "0.28.0",
- "@esbuild/android-arm64": "0.28.0",
- "@esbuild/android-x64": "0.28.0",
- "@esbuild/darwin-arm64": "0.28.0",
- "@esbuild/darwin-x64": "0.28.0",
- "@esbuild/freebsd-arm64": "0.28.0",
- "@esbuild/freebsd-x64": "0.28.0",
- "@esbuild/linux-arm": "0.28.0",
- "@esbuild/linux-arm64": "0.28.0",
- "@esbuild/linux-ia32": "0.28.0",
- "@esbuild/linux-loong64": "0.28.0",
- "@esbuild/linux-mips64el": "0.28.0",
- "@esbuild/linux-ppc64": "0.28.0",
- "@esbuild/linux-riscv64": "0.28.0",
- "@esbuild/linux-s390x": "0.28.0",
- "@esbuild/linux-x64": "0.28.0",
- "@esbuild/netbsd-arm64": "0.28.0",
- "@esbuild/netbsd-x64": "0.28.0",
- "@esbuild/openbsd-arm64": "0.28.0",
- "@esbuild/openbsd-x64": "0.28.0",
- "@esbuild/openharmony-arm64": "0.28.0",
- "@esbuild/sunos-x64": "0.28.0",
- "@esbuild/win32-arm64": "0.28.0",
- "@esbuild/win32-ia32": "0.28.0",
- "@esbuild/win32-x64": "0.28.0"
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/estree-walker": {
@@ -3343,36 +2960,6 @@
"node": ">=8"
}
},
- "node_modules/foreground-child": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
- "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "cross-spawn": "^7.0.6",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/foreground-child/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -3712,16 +3299,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -3799,21 +3376,6 @@
"node": ">=8"
}
},
- "node_modules/istanbul-lib-source-maps": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
- "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.23",
- "debug": "^4.1.1",
- "istanbul-lib-coverage": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/istanbul-reports": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
@@ -3828,22 +3390,6 @@
"node": ">=8"
}
},
- "node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
@@ -3904,6 +3450,279 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/lodash-es": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
@@ -3916,13 +3735,6 @@
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
- "node_modules/loupe": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/lru-cache": {
"version": "11.4.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz",
@@ -3942,18 +3754,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
- "node_modules/magicast": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
- "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.25.4",
- "@babel/types": "^7.25.4",
- "source-map-js": "^1.2.0"
- }
- },
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
@@ -4196,6 +3996,20 @@
"node": ">=4"
}
},
+ "node_modules/obug": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -4256,13 +4070,6 @@
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"license": "MIT"
},
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "dev": true,
- "license": "BlueOak-1.0.0"
- },
"node_modules/partial-json": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz",
@@ -4323,16 +4130,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/pathval": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
- "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.16"
- }
- },
"node_modules/pi-extension-custom-provider-anthropic": {
"resolved": "packages/coding-agent/examples/extensions/custom-provider-anthropic",
"link": true
@@ -4374,9 +4171,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.14",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
- "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -4394,7 +4191,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -4451,24 +4248,23 @@
}
},
"node_modules/protobufjs": {
- "version": "7.5.9",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz",
- "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==",
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz",
+ "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==",
"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.0",
+ "@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.0.0"
+ "long": "^5.3.2"
},
"engines": {
"node": ">=12.0.0"
@@ -4591,58 +4387,40 @@
"node": ">=0.10.0"
}
},
- "node_modules/rollup": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
- "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+ "node_modules/rolldown": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
+ "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.8"
+ "@oxc-project/types": "=0.133.0",
+ "@rolldown/pluginutils": "^1.0.0"
},
"bin": {
- "rollup": "dist/bin/rollup"
+ "rolldown": "bin/cli.mjs"
},
"engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.60.4",
- "@rollup/rollup-android-arm64": "4.60.4",
- "@rollup/rollup-darwin-arm64": "4.60.4",
- "@rollup/rollup-darwin-x64": "4.60.4",
- "@rollup/rollup-freebsd-arm64": "4.60.4",
- "@rollup/rollup-freebsd-x64": "4.60.4",
- "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
- "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
- "@rollup/rollup-linux-arm64-gnu": "4.60.4",
- "@rollup/rollup-linux-arm64-musl": "4.60.4",
- "@rollup/rollup-linux-loong64-gnu": "4.60.4",
- "@rollup/rollup-linux-loong64-musl": "4.60.4",
- "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
- "@rollup/rollup-linux-ppc64-musl": "4.60.4",
- "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
- "@rollup/rollup-linux-riscv64-musl": "4.60.4",
- "@rollup/rollup-linux-s390x-gnu": "4.60.4",
- "@rollup/rollup-linux-x64-gnu": "4.60.4",
- "@rollup/rollup-linux-x64-musl": "4.60.4",
- "@rollup/rollup-openbsd-x64": "4.60.4",
- "@rollup/rollup-openharmony-arm64": "4.60.4",
- "@rollup/rollup-win32-arm64-msvc": "4.60.4",
- "@rollup/rollup-win32-ia32-msvc": "4.60.4",
- "@rollup/rollup-win32-x64-gnu": "4.60.4",
- "@rollup/rollup-win32-x64-msvc": "4.60.4",
- "fsevents": "~2.3.2"
+ "@rolldown/binding-android-arm64": "1.0.3",
+ "@rolldown/binding-darwin-arm64": "1.0.3",
+ "@rolldown/binding-darwin-x64": "1.0.3",
+ "@rolldown/binding-freebsd-x64": "1.0.3",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.3",
+ "@rolldown/binding-linux-arm64-musl": "1.0.3",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-musl": "1.0.3",
+ "@rolldown/binding-openharmony-arm64": "1.0.3",
+ "@rolldown/binding-wasm32-wasi": "1.0.3",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.3",
+ "@rolldown/binding-win32-x64-msvc": "1.0.3"
}
},
- "node_modules/rollup/node_modules/@types/estree": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -4868,13 +4646,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/std-env": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -4885,64 +4656,6 @@
"safe-buffer": "~5.2.0"
}
},
- "node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs": {
- "name": "string-width",
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
@@ -4963,26 +4676,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/strip-literal": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
- "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^9.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/strip-literal/node_modules/js-tokens": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
- "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/strnum": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
@@ -5038,100 +4731,6 @@
"node": ">=6"
}
},
- "node_modules/test-exclude": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
- "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "@istanbuljs/schema": "^0.1.2",
- "glob": "^10.4.1",
- "minimatch": "^10.2.2"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/test-exclude/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/test-exclude/node_modules/brace-expansion": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
- "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/test-exclude/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.2"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/test-exclude/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/test-exclude/node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -5139,17 +4738,10 @@
"dev": true,
"license": "MIT"
},
- "node_modules/tinyexec": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
- "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/tinyglobby": {
- "version": "0.2.16",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
- "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5195,36 +4787,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/tinypool": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
- "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- }
- },
- "node_modules/tinyrainbow": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
- "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tinyspy": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
- "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -5310,9 +4872,9 @@
}
},
"node_modules/undici": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz",
- "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==",
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz",
+ "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==",
"license": "MIT",
"engines": {
"node": ">=22.19.0"
@@ -5332,19 +4894,18 @@
"license": "MIT"
},
"node_modules/vite": {
- "version": "7.3.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",
- "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
+ "version": "8.0.16",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
+ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
- "esbuild": "^0.27.0",
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3",
- "postcss": "^8.5.6",
- "rollup": "^4.43.0",
- "tinyglobby": "^0.2.15"
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.15",
+ "rolldown": "1.0.3",
+ "tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@@ -5360,9 +4921,10 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
- "lightningcss": "^1.21.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
@@ -5375,15 +4937,18 @@
"@types/node": {
"optional": true
},
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
"jiti": {
"optional": true
},
"less": {
"optional": true
},
- "lightningcss": {
- "optional": true
- },
"sass": {
"optional": true
},
@@ -5407,620 +4972,7 @@
}
}
},
- "node_modules/vite-node": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
- "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cac": "^6.7.14",
- "debug": "^4.4.1",
- "es-module-lexer": "^1.7.0",
- "pathe": "^2.0.3",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "bin": {
- "vite-node": "vite-node.mjs"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
- "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-arm": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
- "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
- "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
- "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
- "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/darwin-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
- "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
- "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
- "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-arm": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
- "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
- "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-ia32": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
- "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-loong64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
- "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
- "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
- "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
- "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-s390x": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
- "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
- "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
- "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
- "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
- "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
- "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
- "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/sunos-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
- "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
- "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-ia32": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
- "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
- "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/esbuild": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
- "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.7",
- "@esbuild/android-arm": "0.27.7",
- "@esbuild/android-arm64": "0.27.7",
- "@esbuild/android-x64": "0.27.7",
- "@esbuild/darwin-arm64": "0.27.7",
- "@esbuild/darwin-x64": "0.27.7",
- "@esbuild/freebsd-arm64": "0.27.7",
- "@esbuild/freebsd-x64": "0.27.7",
- "@esbuild/linux-arm": "0.27.7",
- "@esbuild/linux-arm64": "0.27.7",
- "@esbuild/linux-ia32": "0.27.7",
- "@esbuild/linux-loong64": "0.27.7",
- "@esbuild/linux-mips64el": "0.27.7",
- "@esbuild/linux-ppc64": "0.27.7",
- "@esbuild/linux-riscv64": "0.27.7",
- "@esbuild/linux-s390x": "0.27.7",
- "@esbuild/linux-x64": "0.27.7",
- "@esbuild/netbsd-arm64": "0.27.7",
- "@esbuild/netbsd-x64": "0.27.7",
- "@esbuild/openbsd-arm64": "0.27.7",
- "@esbuild/openbsd-x64": "0.27.7",
- "@esbuild/openharmony-arm64": "0.27.7",
- "@esbuild/sunos-x64": "0.27.7",
- "@esbuild/win32-arm64": "0.27.7",
- "@esbuild/win32-ia32": "0.27.7",
- "@esbuild/win32-x64": "0.27.7"
- }
- },
- "node_modules/vite/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
"node_modules/vite/node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/vitest": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
- "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/chai": "^5.2.2",
- "@vitest/expect": "3.2.4",
- "@vitest/mocker": "3.2.4",
- "@vitest/pretty-format": "^3.2.4",
- "@vitest/runner": "3.2.4",
- "@vitest/snapshot": "3.2.4",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
- "chai": "^5.2.0",
- "debug": "^4.4.1",
- "expect-type": "^1.2.1",
- "magic-string": "^0.30.17",
- "pathe": "^2.0.3",
- "picomatch": "^4.0.2",
- "std-env": "^3.9.0",
- "tinybench": "^2.9.0",
- "tinyexec": "^0.3.2",
- "tinyglobby": "^0.2.14",
- "tinypool": "^1.1.1",
- "tinyrainbow": "^2.0.0",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
- "vite-node": "3.2.4",
- "why-is-node-running": "^2.3.0"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@types/debug": "^4.1.12",
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "@vitest/browser": "3.2.4",
- "@vitest/ui": "3.2.4",
- "happy-dom": "*",
- "jsdom": "*"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@types/debug": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- }
- }
- },
- "node_modules/vitest/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
@@ -6074,25 +5026,6 @@
"node": ">=8"
}
},
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -6101,9 +5034,9 @@
"license": "ISC"
},
"node_modules/ws": {
- "version": "8.20.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
- "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "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"
@@ -6156,7 +5089,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -6172,19 +5104,19 @@
},
"packages/agent": {
"name": "@earendil-works/pi-agent-core",
- "version": "0.79.6",
+ "version": "0.80.2",
"license": "MIT",
"dependencies": {
- "@earendil-works/pi-ai": "^0.79.6",
+ "@earendil-works/pi-ai": "^0.80.2",
"ignore": "7.0.5",
"typebox": "1.1.38",
"yaml": "2.9.0"
},
"devDependencies": {
"@types/node": "24.12.4",
- "@vitest/coverage-v8": "3.2.4",
+ "@vitest/coverage-v8": "4.1.9",
"typescript": "5.9.3",
- "vitest": "3.2.4"
+ "vitest": "4.1.9"
},
"engines": {
"node": ">=22.19.0"
@@ -6200,6 +5132,205 @@
"undici-types": "~7.16.0"
}
},
+ "packages/agent/node_modules/@vitest/coverage-v8": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz",
+ "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.1.9",
+ "ast-v8-to-istanbul": "^1.0.0",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "magicast": "^0.5.2",
+ "obug": "^2.1.1",
+ "std-env": "^4.0.0-rc.1",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "4.1.9",
+ "vitest": "4.1.9"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "packages/agent/node_modules/@vitest/expect": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/agent/node_modules/@vitest/pretty-format": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/agent/node_modules/@vitest/runner": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.9",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/agent/node_modules/@vitest/snapshot": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/agent/node_modules/@vitest/spy": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/agent/node_modules/@vitest/utils": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/agent/node_modules/ast-v8-to-istanbul": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz",
+ "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
+ "packages/agent/node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "packages/agent/node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "packages/agent/node_modules/magicast": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
+ "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.3",
+ "@babel/types": "^7.29.0",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "packages/agent/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "packages/agent/node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "packages/agent/node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "packages/agent/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"packages/agent/node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -6207,15 +5338,134 @@
"dev": true,
"license": "MIT"
},
+ "packages/agent/node_modules/vitest": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.9",
+ "@vitest/browser-preview": "4.1.9",
+ "@vitest/browser-webdriverio": "4.1.9",
+ "@vitest/coverage-istanbul": "4.1.9",
+ "@vitest/coverage-v8": "4.1.9",
+ "@vitest/ui": "4.1.9",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "packages/agent/node_modules/vitest/node_modules/@vitest/mocker": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
"packages/ai": {
"name": "@earendil-works/pi-ai",
- "version": "0.79.6",
+ "version": "0.80.2",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "0.91.1",
"@aws-sdk/client-bedrock-runtime": "3.1048.0",
"@google/genai": "1.52.0",
- "@mistralai/mistralai": "2.2.1",
+ "@mistralai/mistralai": "2.2.6",
+ "@opentelemetry/api": "1.9.0",
"@smithy/node-http-handler": "4.7.3",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
@@ -6229,7 +5479,7 @@
"devDependencies": {
"@types/node": "24.12.4",
"canvas": "3.2.3",
- "vitest": "3.2.4"
+ "vitest": "4.1.9"
},
"engines": {
"node": ">=22.19.0"
@@ -6245,6 +5495,176 @@
"undici-types": "~7.16.0"
}
},
+ "packages/ai/node_modules/@vitest/expect": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/ai/node_modules/@vitest/mocker": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "packages/ai/node_modules/@vitest/pretty-format": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/ai/node_modules/@vitest/runner": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.9",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/ai/node_modules/@vitest/snapshot": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/ai/node_modules/@vitest/spy": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/ai/node_modules/@vitest/utils": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/ai/node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "packages/ai/node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "packages/ai/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "packages/ai/node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "packages/ai/node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "packages/ai/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"packages/ai/node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -6252,14 +5672,104 @@
"dev": true,
"license": "MIT"
},
- "packages/coding-agent": {
- "name": "@earendil-works/pi-coding-agent",
- "version": "0.79.6",
+ "packages/ai/node_modules/vitest": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@earendil-works/pi-agent-core": "^0.79.6",
- "@earendil-works/pi-ai": "^0.79.6",
- "@earendil-works/pi-tui": "^0.79.6",
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.9",
+ "@vitest/browser-preview": "4.1.9",
+ "@vitest/browser-webdriverio": "4.1.9",
+ "@vitest/coverage-istanbul": "4.1.9",
+ "@vitest/coverage-v8": "4.1.9",
+ "@vitest/ui": "4.1.9",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "packages/coding-agent": {
+ "name": "@earendil-works/pi-coding-agent",
+ "version": "0.80.2",
+ "license": "MIT",
+ "dependencies": {
+ "@earendil-works/pi-agent-core": "^0.80.2",
+ "@earendil-works/pi-ai": "^0.80.2",
+ "@earendil-works/pi-tui": "^0.80.2",
"@silvia-odwyer/photon-node": "0.3.4",
"chalk": "5.6.2",
"cross-spawn": "7.0.6",
@@ -6273,7 +5783,7 @@
"proper-lockfile": "4.1.2",
"semver": "7.8.0",
"typebox": "1.1.38",
- "undici": "8.3.0",
+ "undici": "8.5.0",
"yaml": "2.9.0"
},
"bin": {
@@ -6289,7 +5799,7 @@
"@types/semver": "7.7.1",
"shx": "0.4.0",
"typescript": "5.9.3",
- "vitest": "3.2.4"
+ "vitest": "4.1.9"
},
"engines": {
"node": ">=22.19.0"
@@ -6300,32 +5810,32 @@
},
"packages/coding-agent/examples/extensions/custom-provider-anthropic": {
"name": "pi-extension-custom-provider-anthropic",
- "version": "0.79.6",
+ "version": "0.80.2",
"dependencies": {
"@anthropic-ai/sdk": "0.52.0"
}
},
"packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": {
"name": "pi-extension-custom-provider-gitlab-duo",
- "version": "0.79.6"
+ "version": "0.80.2"
},
"packages/coding-agent/examples/extensions/gondolin": {
"name": "pi-extension-gondolin",
- "version": "0.79.6",
+ "version": "0.80.2",
"dependencies": {
"@earendil-works/gondolin": "0.12.0"
}
},
"packages/coding-agent/examples/extensions/sandbox": {
"name": "pi-extension-sandbox",
- "version": "1.9.6",
+ "version": "1.10.2",
"dependencies": {
"@anthropic-ai/sandbox-runtime": "0.0.26"
}
},
"packages/coding-agent/examples/extensions/with-deps": {
"name": "pi-extension-with-deps",
- "version": "0.79.6",
+ "version": "0.80.2",
"dependencies": {
"ms": "2.1.3"
},
@@ -6352,6 +5862,149 @@
"undici-types": "~7.16.0"
}
},
+ "packages/coding-agent/node_modules/@vitest/expect": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/coding-agent/node_modules/@vitest/pretty-format": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/coding-agent/node_modules/@vitest/runner": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.9",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/coding-agent/node_modules/@vitest/snapshot": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/coding-agent/node_modules/@vitest/spy": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/coding-agent/node_modules/@vitest/utils": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "packages/coding-agent/node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "packages/coding-agent/node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "packages/coding-agent/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "packages/coding-agent/node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "packages/coding-agent/node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "packages/coding-agent/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"packages/coding-agent/node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -6374,11 +6027,128 @@
},
"engines": {
"node": ">=22.19.0"
+ }
+ },
+ "packages/coding-agent/node_modules/vitest": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.9",
+ "@vitest/browser-preview": "4.1.9",
+ "@vitest/browser-webdriverio": "4.1.9",
+ "@vitest/coverage-istanbul": "4.1.9",
+ "@vitest/coverage-v8": "4.1.9",
+ "@vitest/ui": "4.1.9",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "packages/coding-agent/node_modules/vitest/node_modules/@vitest/mocker": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
}
},
"packages/tui": {
"name": "@earendil-works/pi-tui",
- "version": "0.79.6",
+ "version": "0.80.2",
"license": "MIT",
"dependencies": {
"get-east-asian-width": "1.6.0",
diff --git a/package.json b/package.json
index 537fb1de..c79b42fa 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"@biomejs/biome": "2.3.5",
"@types/node": "22.19.19",
"@typescript/native-preview": "7.0.0-dev.20260120.1",
- "esbuild": "0.28.0",
+ "esbuild": "0.28.1",
"husky": "9.1.7",
"jiti": "2.7.0",
"shx": "0.4.0",
diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md
index 41203247..bec41594 100644
--- a/packages/agent/CHANGELOG.md
+++ b/packages/agent/CHANGELOG.md
@@ -2,6 +2,44 @@
## [Unreleased]
+## [0.80.2] - 2026-06-23
+
+### Changed
+
+- Renamed the public harness shell execution options type from `ExecutionEnvExecOptions` to `ShellExecOptions`.
+
+## [0.80.1] - 2026-06-23
+
+## [0.80.0] - 2026-06-23
+
+### Breaking Changes
+
+- `AgentHarnessOptions.models` is required and is the only auth path: the harness streams turns, compaction, and branch summarization through the provided `Models` instance (`models.streamSimple()`/`completeSimple()`), resolving auth through the providers. `AgentHarnessOptions.getApiKeyAndHeaders` is removed — apps that resolved keys per request now express that as provider auth (`ApiKeyAuth`/`OAuthAuth`) on the providers in the `Models` collection. Build one with `createModels()` + provider factories (or `builtinModels()` from `@earendil-works/pi-ai/providers/all`); tests use `fauxProvider()`.
+- `compact()`, `generateSummary()`, and `generateBranchSummary()` take a `Models` parameter and no longer accept explicit `apiKey`/`headers`.
+- `StreamFn` is defined structurally (`(model, context, options?) => AssistantMessageEventStream | Promise<...>`); `Models.streamSimple` satisfies it.
+- Removed the `@earendil-works/pi-agent-core/base` selective-provider entrypoint; use the root package with an explicit `Models` instance instead.
+
+### Fixed
+
+- Fixed harness session names to normalize newline characters before storing labels ([#5999](https://github.com/earendil-works/pi/pull/5999) by [@haoqixu](https://github.com/haoqixu)).
+- Fixed harness compaction estimates to ignore malformed all-zero assistant usage after truncated responses ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)).
+
+## [0.79.10] - 2026-06-22
+
+## [0.79.9] - 2026-06-20
+
+### Fixed
+
+- Fixed Node execution environment commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)).
+
+## [0.79.8] - 2026-06-19
+
+### Added
+
+- Added `@earendil-works/pi-agent-core/base` for bundlers that want to pair the agent core with selective `@earendil-works/pi-ai/base` provider registration ([#5348](https://github.com/earendil-works/pi/pull/5348) by [@FredKSchott](https://github.com/FredKSchott)).
+
+## [0.79.7] - 2026-06-18
+
## [0.79.6] - 2026-06-16
## [0.79.5] - 2026-06-16
diff --git a/packages/agent/docs/models.md b/packages/agent/docs/models.md
new file mode 100644
index 00000000..15914371
--- /dev/null
+++ b/packages/agent/docs/models.md
@@ -0,0 +1,966 @@
+# Models architecture
+
+This document describes the target design for the next `pi-ai` model/provider refactor. It describes the desired shape, not the current implementation. It is intended to be complete enough to start implementing from a fresh session.
+
+Goals:
+
+- `Models` is a dumb runtime collection of providers.
+- Concrete providers own metadata, auth, model listing, and stream behavior.
+- API implementations live under `src/api/` and are reusable/lazy.
+- Concrete provider factories live under `src/providers/`.
+- Users can import only the providers they need.
+- Importing a provider must not eagerly import heavy SDKs.
+- Dynamic model lists are first-class: reads are sync (last-known list), fetching happens in an explicit async `refresh`.
+- `models.json` and extensions layer by wrapping providers, not by mutating provider internals ad hoc.
+- Old global APIs survive only in an explicit, temporary `/compat` entrypoint.
+
+Non-goals for the immediate `pi-ai` pass:
+
+- Do not migrate coding-agent `ModelRegistry` yet.
+- Do not keep the stream/API registry inside `Models`.
+- Do not implement web OAuth flows yet.
+- Image generation mirrors the chat-side design (`ImagesModels`/`ImagesProvider` in `images-models.ts`); the old global image API (`images.ts`, `images-api-registry.ts`) lives on compat.
+
+## Package layout
+
+Target source layout:
+
+```txt
+packages/ai/src/
+ index.ts # core exports only; no built-in provider imports
+ models.ts # Models runtime, Provider
+ images-models.ts # ImagesModels runtime, ImagesProvider (mirrors models.ts)
+ compat.ts # temporary old-API compatibility entrypoint
+ auth/ # auth method types, helpers, shared resolveProviderAuth(), login callbacks
+ api/ # API implementations and lazy wrappers
+ openai-completions.ts # real implementation, imports SDKs, exports stream/streamSimple
+ openai-completions.lazy.ts
+ openai-responses.ts
+ openai-responses.lazy.ts
+ openai-codex-responses.ts
+ openai-codex-responses.lazy.ts
+ azure-openai-responses.ts
+ azure-openai-responses.lazy.ts
+ anthropic-messages.ts
+ anthropic-messages.lazy.ts
+ google-generative-ai.ts
+ google-generative-ai.lazy.ts
+ google-vertex.ts
+ google-vertex.lazy.ts
+ mistral-conversations.ts
+ mistral-conversations.lazy.ts
+ bedrock-converse-stream.ts
+ bedrock-converse-stream.lazy.ts
+ openrouter-images.ts # image-generation API implementation
+ openrouter-images.lazy.ts
+ lazy.ts # lazyStream()/lazyApi() helpers
+ (shared helpers: openai-responses-shared, google-shared, transform-messages, ...)
+ providers/ # concrete provider factories and per-provider catalogs
+ openai.ts
+ openai.models.ts # generated OpenAI catalog
+ openai-codex.ts
+ openai-codex.models.ts
+ anthropic.ts
+ anthropic.models.ts
+ google.ts
+ google.models.ts
+ ...one pair per built-in provider...
+ openrouter-images.ts # image-generation provider factory
+ faux.ts # test provider factory
+ all.ts # explicit aggregate: builtinModels(), builtinImagesModels(), getBuiltin*()
+ utils/oauth/ # OAuth flow implementations (node), lazy-loaded
+```
+
+`src/index.ts` must stay core-only. It must not import:
+
+- generated model catalogs
+- built-in provider factories
+- provider SDK implementations
+- Node-only OAuth modules
+- `providers/all`
+- `compat`
+
+Provider, API, and compat entrypoints are explicit subpath exports.
+
+## Public usage
+
+Minimal provider usage:
+
+```ts
+import { createModels } from "@earendil-works/pi-ai";
+import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
+
+const models = createModels();
+models.setProvider(openaiProvider());
+
+const model = models.getModel("openai", "gpt-4o-mini");
+if (!model) throw new Error("model not found");
+
+const response = await models.complete(model, context);
+```
+
+Multiple providers:
+
+```ts
+const models = createModels();
+models.setProvider(openaiProvider());
+models.setProvider(openrouterProvider());
+```
+
+All built-ins, explicitly heavy metadata entrypoint:
+
+```ts
+import { builtinModels } from "@earendil-works/pi-ai/providers/all";
+
+const models = builtinModels();
+```
+
+`providers/all` may import all provider metadata/catalogs. It still must not eagerly import SDK implementations; provider streams use lazy wrappers.
+
+## Core runtime: Models
+
+`Models` is a provider collection plus auth application and stream convenience. No stream registry, no auth resolver strategy object.
+
+```ts
+export function createModels(options?: {
+ /** App-owned credential storage. Default: in-memory store. */
+ credentials?: CredentialStore;
+ /** Environment access for auth resolution (env vars, file existence). Default: process.env/node:fs backed; injectable for tests and non-Node hosts. */
+ authContext?: AuthContext;
+}): MutableModels;
+
+export interface Models {
+ getProviders(): readonly Provider[];
+ getProvider(id: string): Provider | undefined;
+
+ /** Sync read of last-known models. Best-effort: a provider whose getModels() throws yields no models. */
+ getModels(provider?: string): readonly Model[];
+ /** Dynamic lists are honestly Model; narrow with the hasApi() guard. */
+ getModel(provider: string, id: string): Model | undefined;
+
+ /**
+ * Ask dynamic providers to re-fetch their model lists. With a provider id,
+ * rejects on that provider's failure; without, refreshes all concurrently
+ * best-effort. Static providers are no-ops.
+ */
+ refresh(provider?: string): Promise;
+
+ /**
+ * Resolve request auth for a model. Includes source label for status UI.
+ * Resolves undefined when the provider is unknown or unconfigured. Rejects
+ * with ModelsError ("oauth" on refresh failure, "auth" on api-key/store
+ * failure); status/availability UIs catch rejections and render
+ * "needs re-login" instead of treating them as unconfigured.
+ */
+ getAuth(model: Model): Promise;
+
+ stream(
+ model: Model,
+ context: Context,
+ options?: ApiStreamOptions,
+ ): AssistantMessageEventStream;
+
+ complete(
+ model: Model,
+ context: Context,
+ options?: ApiStreamOptions,
+ ): Promise;
+
+ streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream;
+ completeSimple(model: Model, context: Context, options?: SimpleStreamOptions): Promise;
+}
+
+export interface MutableModels extends Models {
+ /** Upsert/replace by provider.id. Provider ids are unique. */
+ setProvider(provider: Provider): void;
+ deleteProvider(id: string): void;
+ clearProviders(): void;
+}
+```
+
+Removed concepts:
+
+```txt
+no Models.setStreamFunctions() / getStreamFunctions()
+no api-registry as a real dispatch mechanism
+no Models.provider(id) builder, no setModel/upsertModel/patchModel lifecycle
+no ModelAuthResolver / setAuthResolver — resolution policy is fixed, store is injected
+```
+
+If an app needs different auth policy, it wraps providers (wrap auth methods or `getModels`) or passes explicit request auth in stream options.
+
+## Provider
+
+A provider is the concrete runtime unit. It owns id/name/base metadata, auth methods, model listing, and stream behavior.
+
+`Provider` is generic over the APIs its models use. Concrete factories declare what they emit (`openaiProvider(): Provider<"openai-responses" | "openai-completions">`), giving typed model lists to direct factory users. A `Models` collection holds providers as `Provider`.
+
+```ts
+export interface Provider {
+ readonly id: string;
+ readonly name: string;
+
+ readonly baseUrl?: string;
+ readonly headers?: Record;
+
+ /**
+ * Required: at least one of apiKey/oauth. Even ambient-credential providers
+ * (env vars, AWS profiles, ADC) and keyless local servers provide apiKey
+ * auth whose resolve() reports whether the provider is configured.
+ * getAuth() returning undefined = not configured.
+ */
+ readonly auth: ProviderAuth;
+
+ /** Current known models, sync. Static providers: the catalog. Dynamic providers: as of the last refresh (empty before the first). */
+ getModels(): readonly Model[];
+
+ /** Dynamic providers only: fetch and update the model list. Concurrent calls share one in-flight fetch. */
+ refreshModels?(): Promise;
+
+ stream(model: Model, context: Context, options?: ApiStreamOptions): AssistantMessageEventStream;
+
+ streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream;
+}
+```
+
+There is no `Provider.api` field. `model.api` carries API identity; the provider dispatches internally (see `createProvider()`).
+
+`Model.api` remains: existing metadata and tests use it, it is useful for diagnostics, and provider construction uses it for API implementation selection. But `Models` never dispatches on it; the provider does.
+
+### Typed stream options
+
+Full stream options are API-specific. `Model` pays off by deriving the option type from the API:
+
+```ts
+// types.ts — type-only imports from API impl modules are erased, so this is tree-shake safe
+export interface ApiOptionsMap {
+ "anthropic-messages": AnthropicOptions;
+ "openai-completions": OpenAICompletionsOptions;
+ "openai-responses": OpenAIResponsesOptions;
+ "openai-codex-responses": OpenAICodexResponsesOptions;
+ "azure-openai-responses": AzureOpenAIResponsesOptions;
+ "google-generative-ai": GoogleOptions;
+ "google-vertex": GoogleVertexOptions;
+ "mistral-conversations": MistralOptions;
+ "bedrock-converse-stream": BedrockOptions;
+}
+
+export type ApiStreamOptions = TApi extends keyof ApiOptionsMap
+ ? ApiOptionsMap[TApi]
+ : StreamOptions & Record;
+```
+
+Custom api strings fall back to the generic shape.
+
+### Typed model narrowing
+
+Runtime model lists are dynamic, so `models.getModel()`/`getModels()` honestly return `Model`. Typing improves at three points:
+
+1. **`hasApi()` type guard** — runtime-checked narrowing for dynamic lookups (no blind casts):
+
+ ```ts
+ export function hasApi(model: Model, api: TApi): model is Model;
+
+ const model = models.getModel("anthropic", "claude-opus-4-7");
+ if (model && hasApi(model, "anthropic-messages")) {
+ // model: Model<"anthropic-messages">, stream options fully typed
+ }
+ ```
+
+2. **`getBuiltinModel()`** — sync, generated-catalog lookup with typed overloads: `(provider, id) -> Model`. The path for hardcoded known models.
+
+3. **`Provider` factories** — typed model lists when using a provider directly, without a `Models` collection.
+
+Deliberately not done: tying `models.getModel(provider, ...)` to typed provider/model ids would require statically knowing which providers are installed in a mutable runtime collection. The harness path (`streamSimple` + `SimpleStreamOptions`) is API-agnostic and unaffected.
+
+For comparison: Vercel AI SDK attaches the implementation to the model object, which dissolves dispatch typing but makes models non-serializable (no sessions/RPC/catalogs as plain data), and its `providerOptions` bag is `Record` checked only by `satisfies` convention. Plain-data models + provider-owned behavior keeps stronger typing where it matters.
+
+### Name collision
+
+`types.ts` currently exports `type Provider = KnownProvider | string` (a provider id). Rename that alias to `ProviderId` and fix call sites. The `Provider` interface above takes the name.
+
+## Provider model listing
+
+Reads are sync; fetching is an explicit async verb. `Provider.getModels()` returns the current known list — the full catalog for static providers, the last-refreshed list for dynamic ones (llama.cpp, OpenRouter live listing). `refreshModels()` is where dynamic providers fetch.
+
+This split exists because a sync-or-async union (`Promise | T`) invites latent sync assumptions that detonate on the first async provider, while async-only reads force every consumer (UI lists, extension `find`/`getAll` surfaces) through Promises for data that is almost always static. Sync reads + explicit refresh keeps the staleness visible and the contract single: `getModels()` = last known, `refresh()` = make it current. A fetched list is stale the moment it returns anyway; naming the refresh point is honest about it.
+
+Apps own the refresh lifecycle: startup, registry reload, opening a model selector. Freshness-critical lookups are two-step: `await models.refresh("llamacpp"); models.getModel("llamacpp", id)`.
+
+Dynamic refresh must be side-effect-free discovery:
+
+```txt
+OK: fetch /v1/models, enumerate local catalog, refresh cached remote model list
+Not OK: load model, download model, mutate server state, run request probe
+```
+
+Provider-specific model lifecycle (load/unload) belongs in app/provider-management commands, not in `refreshModels()`.
+
+## Streaming path
+
+`Models.stream()` finds the provider by `model.provider`, resolves auth, merges it into request options, and delegates:
+
+```ts
+function stream(model, context, options) {
+ const provider = this.getProvider(model.provider);
+ if (!provider) {
+ // produce an error stream, not a throw — see Error behavior
+ }
+
+ // async setup happens inside the returned stream (lazyStream pattern)
+ const resolution = await this.getAuth(model);
+ const requestModel = resolution?.auth.baseUrl ? { ...model, baseUrl: resolution.auth.baseUrl } : model;
+ const requestOptions = mergeAuth(options, resolution?.auth); // explicit options win per-field
+
+ return provider.stream(requestModel, context, requestOptions);
+}
+```
+
+`stream()` returns `AssistantMessageEventStream` synchronously; async setup (auth resolution, lazy module load) happens inside the returned stream. The forwarding pattern already exists in today's `register-builtins.ts` (`createLazyStream`); extract it as `lazyStream()` in `src/api/lazy.ts`.
+
+No request hot-path model canonicalization: `stream()` uses the supplied model object as-is. If an app wants fresh model metadata, it refreshes the provider and re-reads (`await models.refresh(p); models.getModel(p, id)`) before starting the turn.
+
+## API implementations under `src/api`
+
+An API implementation is reusable stream behavior. It is not a provider.
+
+Uniform export contract — every real implementation module exports exactly:
+
+```ts
+// src/api/anthropic-messages.ts — imports SDKs
+export function stream(model, context, options) { ... }
+export function streamSimple(model, context, options) { ... }
+```
+
+This makes the module itself satisfy `ProviderStreams`, so the lazy wrapper is one generic helper instead of bespoke per-API plumbing. `ProviderStreams` is the untyped dispatch shape (implementation modules export concretely typed functions, which would not be assignable to a generic method); per-API option typing lives on the modules themselves and on `Provider.stream()` via `ApiStreamOptions`:
+
+```ts
+export interface ProviderStreams {
+ stream(model: Model, context: Context, options?: StreamOptions): AssistantMessageEventStream;
+ streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream;
+}
+
+// src/api/lazy.ts
+export function lazyApi(load: () => Promise): ProviderStreams;
+
+// src/api/anthropic-messages.lazy.ts
+export const anthropicMessagesApi = (): ProviderStreams => lazyApi(() => import("./anthropic-messages.ts"));
+```
+
+Import chain:
+
+```txt
+provider module -> lazy API wrapper -> dynamic import(real API impl) -> SDK deps
+```
+
+Notes:
+
+- Bedrock keeps the node-only dynamic import trick (`importNodeOnlyProvider`, `.ts`/`.js` specifier rewrite) inside its lazy wrapper. `setBedrockProviderModule()` (used by the Bun build) moves into the bedrock lazy wrapper module.
+- Shared helper modules (`openai-responses-shared.ts`, `google-shared.ts`, `transform-messages.ts`, prompt-cache, copilot headers) move to `src/api/` alongside the implementations.
+
+## Shared API implementations across concrete providers
+
+Many concrete providers share an API implementation (OpenAI-completions: OpenRouter, Groq, Cerebras, xAI, ZAI, ...). They share lazy API objects by reference:
+
+```ts
+import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
+
+export function openrouterProvider(): Provider {
+ return createProvider({
+ id: "openrouter",
+ name: "OpenRouter",
+ baseUrl: "https://openrouter.ai/api/v1",
+ auth: { apiKey: envApiKeyAuth("OpenRouter API key", ["OPENROUTER_API_KEY"]) },
+ models: OPENROUTER_MODELS,
+ api: openAICompletionsApi(),
+ });
+}
+```
+
+This copies Vercel AI SDK's useful property: users import concrete providers; shared protocol implementation is internal.
+
+## Auth
+
+Request auth output stays small:
+
+```ts
+export interface ModelAuth {
+ apiKey?: string;
+ headers?: Record;
+ baseUrl?: string;
+}
+```
+
+If a value cannot be expressed as `apiKey`, `headers`, or `baseUrl`, it is provider config, not auth (Vertex project/location, Bedrock region/profile, Azure apiVersion are provider factory options).
+
+### Provider auth
+
+`Provider.auth` has exactly two slots; real providers have at most one api-key path and at most one OAuth path, and the slot names carry the UI's oauth-vs-api-key split without a `kind` discriminant or method ids:
+
+```ts
+export interface ProviderAuth {
+ apiKey?: ApiKeyAuth; // stored key/provider env + ambient env/files/ADC/IAM
+ oauth?: OAuthAuth; // login flow + refresh
+}
+
+export interface ApiKeyAuth {
+ name: string; // "Anthropic API key"
+
+ /** Interactive setup (prompt for key/provider env). Absent = ambient-only (env, ADC, IAM). */
+ login?(callbacks: AuthLoginCallbacks): Promise;
+
+ /**
+ * Resolve auth from the stored credential and/or ambient sources, merging
+ * per field (credential.key ?? env("..."), credential.env?.NAME ?? env("...")).
+ * undefined = not configured.
+ */
+ resolve(input: {
+ model: Model;
+ ctx: AuthContext;
+ credential?: ApiKeyCredential;
+ }): Promise;
+}
+
+export interface OAuthAuth {
+ name: string; // "Anthropic (Claude Pro/Max)"
+
+ login(callbacks: AuthLoginCallbacks): Promise;
+
+ /** Exchange the refresh token. Network call; throws on failure (invalid_grant etc.). Runs under the store lock. */
+ refresh(credential: OAuthCredential): Promise;
+
+ /** Side-effect-free derivation of request auth from a valid credential. Covers Copilot-style per-credential baseUrl. Async so lazy wrappers can load the implementation. */
+ toAuth(credential: OAuthCredential): Promise;
+}
+
+export interface AuthResult {
+ auth: ModelAuth;
+ /** Human-readable label for status UI: "ANTHROPIC_API_KEY", "OAuth", "~/.aws/credentials". */
+ source?: string;
+}
+
+export interface AuthContext {
+ env(name: string): Promise;
+ fileExists(path: string): Promise; // supports leading ~
+}
+```
+
+The OAuth split (`refresh` + `toAuth` instead of one `resolve`) matches the old `OAuthProviderInterface` (`refreshToken` + `getApiKey`) and lets `Models` own the locking pattern without closure gymnastics: refresh produces a credential, `toAuth` derives request auth from whatever credential ends up stored.
+
+There is no `usesCallbackServer` flag. With `prompt()/notify()` callbacks the flow self-describes at runtime: a flow that runs a callback server issues a `manual_code` prompt racing the server and aborts the prompt when the callback wins. The UI needs no static foreknowledge.
+
+### Credentials
+
+One credential per provider, type-tagged — exactly the shape of today's auth.json (`type: "api_key" | "oauth"` per provider id):
+
+```ts
+export interface ApiKeyCredential {
+ type: "api_key";
+ key?: string;
+ env?: ProviderEnv; // e.g. Cloudflare account/gateway ids, Azure/Vertex/Bedrock scoped config
+}
+
+export interface OAuthCredential extends OAuthCredentials {
+ type: "oauth"; // access, refresh, expires from OAuthCredentials
+}
+
+export type Credential = ApiKeyCredential | OAuthCredential;
+```
+
+`ApiKeyCredential.env` stores provider-scoped environment/config values alongside or instead of a key. `ApiKeyAuth.resolve()` merges per field: `credential.key ?? env("CLOUDFLARE_API_KEY")`, `credential.env?.CLOUDFLARE_ACCOUNT_ID ?? env("CLOUDFLARE_ACCOUNT_ID")`, etc. The credential discriminator intentionally matches today's `auth.json` (`api_key`) so the file-backed store does not need lossy type translation.
+
+### Credential store
+
+The app injects storage; `pi-ai` ships an in-memory default. Keyed by provider id, one credential per provider:
+
+```ts
+export interface CredentialStore {
+ /** Read the stored credential, possibly expired. Display/status use; request auth comes from Models.getAuth(). */
+ read(providerId: string): Promise;
+
+ /**
+ * Serialized write — the only write path. fn sees the current credential
+ * because correct writes (refresh, login-during-refresh) depend on it;
+ * return the new credential, or undefined to leave the entry unchanged.
+ * Mutual exclusion per provider id, cross-process too where the backing
+ * store supports it (file lock). Resolves with the post-write credential.
+ */
+ modify(
+ providerId: string,
+ fn: (current: Credential | undefined) => Promise,
+ ): Promise;
+
+ /** Remove (logout). Serialized against modify. */
+ delete(providerId: string): Promise;
+}
+```
+
+There is deliberately no `set`: an unserialized write path invites read-modify-write races (login-during-refresh clobbering a fresh credential, double token refresh). Call sites:
+
+```ts
+await store.modify(pid, async () => credential); // login: store this
+await store.read(pid); // status UI ("logged in via OAuth")
+await store.delete(pid); // logout
+// refresh RMW happens inside Models.getAuth
+```
+
+Error semantics: `read` resolves `undefined` for missing entries; methods reject only on storage failure, and `Models` wraps such rejections in `ModelsError` code `"auth"`. Best-effort stores that serve an in-memory view and record persistence errors internally (today's AuthStorage behavior) are valid implementations.
+
+### Resolution policy (fixed)
+
+`Models.getAuth(model)` is a decision tree, not a loop. A stored credential owns the provider — ambient/env is consulted only when nothing is stored (AuthStorage parity: no silent env fallback after a failed refresh or for an unmatched credential type):
+
+```ts
+const stored = await store.read(provider.id);
+if (stored) {
+ if (stored.type === "oauth" && provider.auth.oauth) {
+ const oauth = provider.auth.oauth;
+ let credential = stored;
+ if (Date.now() >= credential.expires) { // optimistic check, lock-free
+ const post = await store.modify(provider.id, async (current) => {
+ if (current?.type !== "oauth") return undefined; // logged out meanwhile
+ return Date.now() >= current.expires // authoritative check, under lock
+ ? oauth.refresh(current) // throws -> ModelsError("oauth")
+ : undefined; // another process/request refreshed
+ });
+ if (post?.type !== "oauth") return undefined;
+ credential = post;
+ }
+ return { auth: await oauth.toAuth(credential), source: "OAuth" };
+ }
+ if (stored.type === "api_key" && provider.auth.apiKey) {
+ return provider.auth.apiKey.resolve({ model, ctx, credential: stored });
+ }
+ return undefined; // stored credential without matching handler blocks ambient
+}
+return provider.auth.apiKey?.resolve({ model, ctx, credential: undefined }); // ambient
+```
+
+Properties:
+
+- Double-checked locking, same as today's `refreshOAuthTokenWithLock`: valid tokens cost one `read` and zero locks; expired tokens lock, re-check under the lock, refresh once globally, persist before release.
+- Explicit request auth (stream options `apiKey`/`headers`) is merged per-field on top in `stream()`, winning over everything.
+- Refresh failure rejects with `ModelsError("oauth")`; the stored credential is untouched (preserved for retry). Request paths surface this as a stream error with the real cause ("run /login"); status/availability UIs catch the rejection and render "needs re-login" — documented contract on `getAuth`.
+
+### Replacing AuthStorage
+
+The end state for coding-agent: AuthStorage is deleted; its capabilities map onto a `CredentialStore` implementation plus composition.
+
+Today's `getApiKey` priority and its new home:
+
+| AuthStorage today | New design |
+|---|---|
+| runtime override (CLI `--api-key`) | `withRuntimeOverrides(store, overrides)` decorator: `read` returns the override as an `ApiKeyCredential`; never persisted |
+| stored `api_key` (with `$ENV`/`!command` via `resolveConfigValue`) | stored `ApiKeyCredential`; config-value resolution happens at `read` in coding-agent's adapter/decorator (command execution stays app policy) |
+| stored `oauth` + locked refresh, undefined on failure | `getAuth` decision tree above; failure rejects with cause instead of silently unconfiguring |
+| env var (only when nothing stored) | ambient branch of `apiKey.resolve` |
+| `fallbackResolver` (models.json custom providers) | gone — custom providers carry their own `auth.apiKey` |
+
+```txt
+FileCredentialStore ports AuthStorage's lock backend: read = memory snapshot,
+ modify = withLockAsync(re-read, fn, merge-write), delete,
+ internal error recording (drainErrors equivalent)
+└─ withConfigValues $ENV / !command at read
+ └─ withRuntimeOverrides --api-key
+ └─ createModels({ credentials: store })
+
+login/logout UI provider.auth.{oauth,apiKey}.login(callbacks) + store.modify/delete
+status UI store.read(pid) + getAuth try/catch ("needs /login" on rejection)
+getOAuthProviders presence of provider.auth.oauth across registered providers
+```
+
+### Login callbacks
+
+One interface serves api-key and OAuth login:
+
+```ts
+export interface AuthLoginCallbacks {
+ /** Aborts the whole login flow. Per-prompt cancellation uses AuthPrompt.signal. */
+ signal?: AbortSignal;
+
+ prompt(prompt: AuthPrompt): Promise;
+ notify(event: AuthEvent): void;
+}
+
+/** `signal` lets the flow cancel a pending prompt when an out-of-band event resolves the step. */
+export type AuthPrompt = { signal?: AbortSignal } & (
+ | { type: "text"; message: string; placeholder?: string }
+ | { type: "secret"; message: string; placeholder?: string }
+ | { type: "select"; message: string; options: readonly { id: string; label: string; description?: string }[] }
+ | { type: "manual_code"; message: string; placeholder?: string }
+);
+
+export type AuthEvent =
+ | { type: "auth_url"; url: string; instructions?: string }
+ | { type: "device_code"; userCode: string; verificationUri: string; intervalSeconds?: number; expiresInSeconds?: number }
+ | { type: "progress"; message: string };
+```
+
+`prompt()` returns the entered/selected string (`select` returns the option id). Flows race a `manual_code` prompt against a callback server by setting `AuthPrompt.signal` and aborting the prompt when the callback wins.
+
+### OAuth attachment
+
+Providers that support OAuth always attach it. There is no factory toggle: the flow is lazy-loaded, so advertising OAuth costs nothing until `login()`/`refresh()` actually runs, and a host that never logs in never loads it.
+
+```ts
+export function anthropicProvider(): Provider {
+ return createProvider({
+ id: "anthropic",
+ name: "Anthropic",
+ baseUrl: "https://api.anthropic.com/v1",
+ auth: {
+ apiKey: envApiKeyAuth("Anthropic API key", ["ANTHROPIC_API_KEY"]),
+ oauth: lazyOAuth({
+ name: "Anthropic (Claude Pro/Max)",
+ load: () => import("../utils/oauth/anthropic.ts").then((m) => m.anthropicOAuth),
+ }),
+ },
+ models: ANTHROPIC_MODELS,
+ api: anthropicMessagesApi(),
+ });
+}
+```
+
+`lazyOAuth()` wraps a dynamically imported `OAuthAuth` so provider definitions can advertise OAuth without importing the implementation (`toAuth` is async for exactly this reason):
+
+```ts
+export function lazyOAuth(input: {
+ name: string;
+ load: () => Promise;
+}): OAuthAuth;
+```
+
+OAuth must not force Node-only code (`node:http`, `node:crypto`) into browser bundles: the dynamic import inside `lazyOAuth()` uses the same bundler-opaque variable-specifier trick as the bedrock lazy wrapper. Browser hosts never trigger the load (no stored node OAuth credentials, no login flow). If web OAuth lands later (sitegeist proved feasibility: Web Crypto PKCE, auth tab, fetch token exchange, device-code polling), it is just a different `OAuthAuth` implementation — no reserved option values.
+
+The existing flows in `src/utils/oauth/` (anthropic, openai-codex, github-copilot) are adapted to `OAuthAuth` (`login`/`refresh`/`toAuth`, replacing `login`/`refreshToken`/`getApiKey`/`modifyModels`) with the new callbacks, staying Node-targeted and lazy-loaded. Copilot's `modifyModels` baseUrl rewriting becomes `toAuth` returning `ModelAuth.baseUrl`.
+
+## Provider wrappers and models.json
+
+`models.json` is a provider wrapper layer. It does not mutate providers in place:
+
+```ts
+function withProviderOverrides(base: Provider, overrides: ProviderOverrides): Provider {
+ return {
+ ...base,
+ name: overrides.name ?? base.name,
+ baseUrl: overrides.baseUrl ?? base.baseUrl,
+ headers: mergeHeaders(base.headers, overrides.headers),
+
+ getModels: () => applyModelOverrides(base.getModels(), overrides.models),
+ refreshModels: base.refreshModels?.bind(base),
+
+ stream: base.stream,
+ streamSimple: base.streamSimple,
+ };
+}
+```
+
+This composes with dynamic providers because `getModels()` delegates to the base source and `refreshModels()` passes through.
+
+Request-auth config from models.json (`$ENV`, `!command`, inline keys) remains app-owned sidecar state, surfaced either as explicit request auth or as a custom `ApiKeyAuth` the app sets on the wrapped provider's `auth.apiKey`.
+
+## Custom providers: createProvider()
+
+One helper builds providers from parts; it handles both single-API and mixed-API providers:
+
+```ts
+export function createProvider(input: {
+ id: string;
+ name?: string; // default: id
+ baseUrl?: string;
+ headers?: Record;
+ auth: ProviderAuth; // required, at least one of apiKey/oauth (no "no-auth" providers)
+ /** Initial model list (empty for purely dynamic providers). */
+ models: readonly Model[];
+ /** Dynamic providers: fetch the current list; createProvider stores it and dedupes in-flight calls. */
+ refreshModels?: () => Promise[]>;
+ /** Single implementation, or map keyed by model.api for mixed-API providers. */
+ api: ProviderStreams | Record;
+}): Provider;
+```
+
+- Single `api`: all models stream through it.
+- Map `api`: `stream()`/`streamSimple()` dispatch on `model.api`; unknown api produces a stream error.
+
+Mixed-API custom providers must be supported (opencode Go/Zen-style providers expose models backed by different APIs under one provider id).
+
+Built-in provider factories use `createProvider()` internally. models.json custom providers map onto it directly:
+
+```json
+{
+ "providers": {
+ "my-openai-proxy": {
+ "api": "openai-completions",
+ "baseUrl": "https://proxy.example/v1",
+ "models": [ ... ]
+ }
+ }
+}
+```
+
+## Compat entrypoint
+
+`@earendil-works/pi-ai/compat` preserves the old global API surface until the coding-agent migration deletes it. New code never imports it.
+
+Old semantics being preserved: global `stream()` can still dispatch by `model.api` through the legacy api-registry for custom providers, mutated models, and tests/extensions that override a built-in API implementation.
+
+- `stream/complete/streamSimple/completeSimple(model, ctx, opts)`: real built-in provider/model/api matches route through a singleton `builtinModels()` collection, so provider auth/env/baseUrl behavior is shared with the new runtime. Unknown providers, mutated models, or overridden API registrations fall back to api-registry dispatch plus `getEnvApiKey` injection.
+- The builtin api registration side effect moves from the root barrel into compat. It skips api ids that already have a registration, since compat may load after a test or extension has already registered an override. `registerApiProvider()/unregisterApiProviders()` keep feeding the compat-local registry; `resetApiProviders()` clears and re-registers builtins.
+- Sync `getModel/getModels/getProviders` are deprecated aliases of `getBuiltinModel/getBuiltinModels/getBuiltinProviders` from `providers/all` (they were always pure generated-catalog reads — verified: nothing ever mutated the old `modelRegistry`).
+- Re-exports the per-API lazy stream wrappers (incl. `setBedrockProviderModule`), `env-api-keys.ts`, and the image-generation registry/catalogs; none of these stay on the root barrel.
+- `export * from "./index.ts"`: compat is a strict superset of the core entrypoint, so consumers switch a file's import path wholesale without symbol surgery.
+
+coding-agent (and the interim agent package) switch imports of these symbols from `@earendil-works/pi-ai` to `@earendil-works/pi-ai/compat` (import-path-only change) and are otherwise untouched until the ModelManager migration.
+
+Extension grace period: the coding-agent extension loader (jiti aliases + Bun `virtualModules`) resolves the `@earendil-works/pi-ai` ROOT specifier to the compat entrypoint. Existing user extensions using the old global API (`complete`, `getModel`, `registerApiProvider`, ...) keep working at runtime without changes; they break only when compat is removed at the ModelManager migration, with a migration guide in the changelog. Typechecking is the nudge: editors resolve the root to the slim core types, so extension sources that typecheck must import old globals from `/compat` — which is what the repo example extensions demonstrate.
+
+## Builtin static helpers
+
+Typed, sync, generated-catalog-only helpers live with the catalogs (exported from `providers/all`):
+
+```ts
+getBuiltinModel(provider, id) // sync, typed overloads from generated catalog
+getBuiltinModels(provider) // sync
+getBuiltinProviders() // sync
+```
+
+Runtime lookup through a `Models` instance is sync over the last-known provider lists: `models.getModel(...)`. Freshness-critical callers run `await models.refresh(provider)` first.
+
+Generated catalogs are split per provider (`providers/.models.ts`) by updating `packages/ai/scripts/generate-models.ts`. If the generator change turns out too large for this pass, splitting may be deferred; `providers/all` and provider factories may temporarily import the monolithic `models.generated.ts`, relying on `sideEffects: false` for pruning.
+
+## Tree-shaking and lazy imports
+
+Rules:
+
+1. Main `@earendil-works/pi-ai` import is core-only.
+2. Provider modules import their catalog, auth helpers, and lazy API wrappers only.
+3. Lazy API wrappers dynamically import real API implementations.
+4. Real API implementations import SDK dependencies.
+5. OAuth implementations are always attached via `lazyOAuth()` and lazy-loaded behind a bundler-opaque dynamic import; provider metadata never eagerly imports Node-only OAuth code.
+6. `providers/all` imports every built-in provider factory and all catalogs. It is the explicit heavy entrypoint.
+7. Provider modules are side-effect-free; importing a provider does not register anything globally.
+8. `package.json` lists only effectful compat/image registration files in `sideEffects`; root and provider modules stay tree-shakeable.
+9. With code splitting, provider SDKs stay in lazy chunks. Without code splitting, bundlers fold statically reachable lazy API implementations into the single bundle; `providers/all` then pulls all statically visible SDKs. Bedrock is the exception because its AWS SDK implementation is behind a bundler-opaque Node-only import and needs `setBedrockProviderModule()` for standalone single-file bundles.
+
+Exports map sketch:
+
+```json
+{
+ "exports": {
+ ".": "./dist/index.js",
+ "./compat": "./dist/compat.js",
+ "./providers/all": "./dist/providers/all.js",
+ "./providers/openai": "./dist/providers/openai.js",
+ "./providers/anthropic": "./dist/providers/anthropic.js",
+ "./providers/*": "./dist/providers/*.js",
+ "./api/*": "./dist/api/*.js"
+ }
+}
+```
+
+Browser smoke check (`scripts/check-browser-smoke.mjs`) must keep passing: bundling the core entrypoint (and any non-node provider entrypoint) must not pull `node:http`/`node:crypto`.
+
+## AgentHarness integration
+
+`AgentHarness` receives a `Models` instance.
+
+- `AgentHarnessOptions.models` is required.
+- The harness does not snapshot `Models` into turn state.
+- Request path calls `this.models.streamSimple(model, context, options)`; same for compaction/branch-summarization paths.
+- Request path never calls async `models.getModel()` to canonicalize; if model metadata needs refresh, the app updates the selected model before starting a turn.
+- Harness tests build `createModels()` and install the faux provider (`fauxProvider()` factory from `providers/faux`).
+
+## coding-agent next phase (not this pass)
+
+coding-agent builds providers in layers and binds them per session:
+
+```txt
+built-in providers (builtinModels)
+-> models.json provider wrappers / custom providers (createProvider)
+-> extension provider wrappers/additions
+```
+
+```ts
+sessionModels.clearProviders();
+for (const provider of layeredProviders) sessionModels.setProvider(provider);
+```
+
+coding-agent owns: `FileCredentialStore` + decorators replacing AuthStorage (see "Replacing AuthStorage"), models.json auth sidecar (`$ENV`, `!command`), command execution policy, provider status labels (from `AuthResult.source`), login/logout UI (driving `auth.{apiKey,oauth}.login()` with `prompt()/notify()`), extension lifecycle, provider-management slash commands.
+
+Current interim state:
+
+- `AgentHarness` already accepts a `Models` instance and uses it for turn streaming, compaction, and branch summaries.
+- coding-agent does not use `AgentHarness` yet; `AgentSession` still drives the low-level `Agent` with a `streamFn`.
+- coding-agent still uses legacy `AuthStorage` + `ModelRegistry` and imports old global pi-ai APIs through `@earendil-works/pi-ai/compat`.
+- The extension loader still aliases the pi-ai root to `/compat` as the runtime grace period for old extensions.
+
+## Implementation TODOs
+
+Check items off as they land. Keep this list current; it is the working state for resumed sessions.
+
+### Phase 1 — core types/runtime
+
+- [x] Rename `types.ts` `Provider` alias to `ProviderId`; fix call sites.
+- [x] Add `ApiOptionsMap` and `ApiStreamOptions` to `types.ts` (type-only imports).
+- [x] New `models.ts`: `Provider` interface, `hasApi()` guard, `ModelsError` + codes. Auth types live in `src/auth/types.ts` (`ProviderAuth` = `{ apiKey?, oauth? }`, credentials, `CredentialStore` (`read`/`modify`/`delete`, one credential per provider), `AuthResult`, `AuthContext`, `ModelAuth`, login callbacks), in-memory store in `src/auth/credential-store.ts`, default context in `src/auth/context.ts` (browser-safe node:fs trick), `lazyStream()` in `src/api/lazy.ts`.
+- [x] `Models`/`MutableModels`/`createModels({ credentials?, authContext? })` with provider map, sync `getModel(s)` (per-provider failure isolation), explicit async `refresh(provider?)`, `getAuth` (decision tree, double-checked locked refresh), `stream/complete/streamSimple/completeSimple` with per-field auth merge. Tests: `packages/ai/test/models-runtime.test.ts`.
+- [x] Keep metadata helpers: `calculateCost`, `getSupportedThinkingLevels`, `clampThinkingLevel`, `modelsAreEqual`.
+
+### Phase 2 — `src/api/`
+
+- [x] Move stream implementations from `src/providers/` to `src/api/`, renamed by API id (`anthropic.ts` -> `api/anthropic-messages.ts`, etc.).
+- [x] Normalize each implementation module to export exactly `stream` and `streamSimple`.
+- [x] Move shared helpers (`openai-responses-shared`, `google-shared`, `transform-messages`, `openai-prompt-cache`, `github-copilot-headers`, `cloudflare`, `simple-options`) to `src/api/`.
+- [x] Extract `lazyStream()`/`lazyApi()` into `src/api/lazy.ts`.
+- [x] Add `*.lazy.ts` wrappers per API; bedrock keeps node-only import trick and `setBedrockProviderModule()`.
+- [x] Delete `providers/register-builtins.ts`. Interim until Phase 5 compat: builtin api-registry registration lives in `stream.ts`; lazy API wrappers are exported from the root barrel.
+
+### Phase 3 — provider factories + catalogs
+
+- [x] Auth helpers in `src/auth/helpers.ts`: `envApiKeyAuth()` (with secret-prompt `login`), `lazyOAuth()`. OAuth flow loads go through `utils/oauth/load.ts` (bundler-opaque dynamic import); the `OAuthAuth` exports it references land in Phase 4.
+- [x] `createProvider()` in `models.ts` (single + mixed `api` map, dispatch on `model.api`, unknown api -> stream error).
+- [x] Per-provider factories under `src/providers/` for all built-in catalog providers; OAuth attached via `lazyOAuth()` (anthropic, openai-codex, github-copilot); ambient `ApiKeyAuth` for amazon-bedrock (AWS env/profile) and google-vertex (key or ADC+project+location).
+- [x] `providers/all.ts`: `builtinProviders()`, `builtinModels()`, `getBuiltinModel/getBuiltinModels/getBuiltinProviders` re-exports.
+- [x] Faux provider factory (`fauxProvider()` in `providers/faux.ts`) for tests; legacy `registerFauxProvider()` kept until compat dies.
+- [x] Split generated catalogs per provider via `scripts/generate-models.ts` (`providers/.models.ts`); `models.generated.ts` becomes a generated aggregator.
+
+### Phase 4 — OAuth adaptation
+
+- [x] Adapt `utils/oauth/anthropic.ts`, `openai-codex.ts`, `github-copilot.ts` to `OAuthAuth` (`login`/`refresh`/`toAuth`) + `prompt()/notify()`; `modifyModels` baseUrl rewriting becomes `toAuth().baseUrl`. New exports (`anthropicOAuth`, `openaiCodexOAuth`, `githubCopilotOAuth`) sit next to the old `OAuthProviderInterface` objects, which survive until Phase 7.
+- [x] No `usesCallbackServer` on `OAuthAuth`: callback-server flows race a `manual_code` prompt (aborted via `AuthPrompt.signal` once the flow settles). The old interface keeps its flag until it dies with compat.
+
+### Phase 5 — packaging
+
+- [x] `index.ts` core-only and side-effect free (no catalogs, no provider factories, no api-registry, no env-api-keys, no images, no OAuth, no compat). Typed catalog reads (`getBuiltin*`) implemented in `providers/all.ts`; `models.ts` no longer imports `models.generated.ts`.
+- [x] `compat.ts`: superset of index + old api-dispatch globals, deprecated `getModel/getModels/getProviders` aliases, lazy api wrappers + `setBedrockProviderModule`, `getEnvApiKey`, images. Registration side effect lives here (skip-if-present).
+- [x] Subpath exports map (`./compat`, `./providers/*`, `./api/*`); `sideEffects` array listing the effectful modules (`compat`, images registration) instead of `false`.
+- [x] Browser smoke (entry now imports old globals from `/compat`) + shrinkwrap checks green. Internal old-global imports switched to `/compat` already (42 files in agent/coding-agent/examples; vitest configs alias `/compat` to src; spawn-CLI tests resolve workspace dist, so `packages/ai` + `packages/agent` dists were rebuilt).
+
+### Phase 6 — AgentHarness
+
+- [x] `AgentHarnessOptions.models` required (`readonly models` on the harness); the harness stream path uses `models.streamSimple()`. `StreamFn` redefined structurally (no compat type dependency); `Models.streamSimple` satisfies it.
+- [x] Compaction/branch-summarization take the harness `Models` instance. `getApiKeyAndHeaders` is removed entirely — `Models` is the only auth path; per-request key resolution becomes provider auth on the collection. `compact()`/`generateSummary()`/`generateBranchSummary()` lose their explicit `apiKey`/`headers` parameters.
+- [x] Harness tests use `createModels()` + `fauxProvider()` with unique per-fake provider ids; no global api-registry state, no unregister bookkeeping.
+
+### Phase 7 — coding-agent bridge (minimal)
+
+- [x] Switch old-global imports to `@earendil-works/pi-ai/compat` (landed with Phase 5; compat is a superset so the switch was path-only). Extension loader resolves the pi-ai root to compat as the runtime grace period.
+- [x] Everything else originally sketched here is gated on coding-agent actually streaming through a `Models` instance — coding-agent's `AgentSession` drives the low-level `Agent` via `streamFn`, not the harness — and moved to Phase 9.
+
+### Phase 8 — wrap-up
+
+- [x] Update/add tests; run affected suites (tests landed with each phase; `./test.sh` green throughout).
+- [x] `packages/ai/CHANGELOG.md`: `### Breaking Changes` with migration guide (compat entrypoint, `Provider` -> `ProviderId`, api module moves) + `### Added` for the new Models/provider/auth API.
+- [x] `packages/coding-agent/CHANGELOG.md`: `### Changed` entry for extension authors — runtime unaffected (loader resolves the pi-ai root to compat), typecheck nudges to `/compat` or the new API; removal happens later with a migration guide.
+- [x] `packages/agent/CHANGELOG.md`: `### Breaking Changes` for required `AgentHarnessOptions.models`, compaction signature changes, structural `StreamFn`.
+- [x] `npm run check` clean.
+
+### Phase 9 — coding-agent on Models + CredentialStore (in scope)
+
+coding-agent replaces AuthStorage and ModelRegistry's internals with `FileCredentialStore` + a `MutableModels` collection. AgentSession itself stays (AgentHarness adoption is pi 2.0); only its model/auth substrate swaps. Layering is strictly one-directional:
+
+```txt
+FileCredentialStore (auth.json, locked, $ENV/!command resolution) + explicit --api-key overlay
+ ↑
+MutableModels: builtin factories (wrapped per models.json config) + custom providers (models.json ∪ extensions)
+ ↑
+ModelRegistry: compatibility facade — sync last-known reads delegate to the collection; registerProvider/login/logout/status for extensions + UI
+ ↑
+AgentSession / sdk / interactive-mode (stream via models; await only auth/refresh paths)
+```
+
+Decisions:
+
+- `AuthStorage` is deleted as a type — it would otherwise depend on provider auth while provider auth depends on its store (circular). Its surface splits: `get`/`set`/`remove` -> `CredentialStore`; `getApiKey` -> `Models.getAuth`; `login`/`logout`/`getAuthStatus` -> ModelRegistry facade methods over `provider.auth.oauth` + the store.
+- `FileCredentialStore` is self-contained (path, locking, parse/write, chmod, error buffering) and owns `auth.json` semantics, including `$ENV`/`!command` resolution for stored API-key credentials. Persisted values stay raw; resolution returns copies for auth use.
+- Runtime `--api-key` overrides are an explicit store overlay (an override reads as an ephemeral stored api-key credential, masking stored OAuth — matches today's priority). Every registered provider is guaranteed an `apiKey` auth slot so overrides apply to OAuth-only providers too.
+- `ModelRegistry.getAll`/`find`/`getAvailable` stay sync for SDK and extension compatibility, delegating to the collection's last-known sync model lists and fast configured-looking status checks. Dynamic providers update through explicit async `refresh()`, and request auth remains async through `getApiKeyAndHeaders()`/`Models.getAuth()`. Extensions also get the collection itself as the forward API.
+- models.json keeps FULL feature parity, implemented as provider decoration: builtin factories wrapped so `getModels()` applies provider `baseUrl`/`compat` overlays, `modelOverrides`, and custom-model merges (async-safe); provider `apiKey`/`headers`/`authHeader` configs become that provider's `ApiKeyAuth` (config first, factory auth fallback); parse errors keep `getError()` semantics.
+- Extension `ProviderConfig` parity: provider-keyed `streamSimple`, old-style `oauth` adapted to `OAuthAuth` (`modifyModels` -> `getModels` wrap + `toAuth`), full model replacement per provider. Legacy `registerApiProvider` writes stay compat-local for consumers that call global `complete()`; they die with compat.
+- Copilot: stored-credential baseUrl applied in the wrapped `getModels()` (extension-visible models stay correct) plus per-request `toAuth().baseUrl`.
+- Cloudflare: provider-auth substitution (key + `CLOUDFLARE_ACCOUNT_ID`/`CLOUDFLARE_GATEWAY_ID` from credential `env` or ambient `AuthContext.env()` -> `ModelAuth.baseUrl`). Built-in compat calls route through `Models`, so they use the same provider auth path.
+
+Ordering for new sessions:
+
+1. [x] pi-ai rework first: `Provider.getModels()` sync + optional `refreshModels()`; `Models.getModels`/`getModel` sync, `Models.refresh(provider?)` async; `createProvider` takes `models` array + optional `refreshModels` fetcher (in-flight dedupe). Reverses Phase 1's async-listing decision — see "Provider model listing" for rationale (sync-or-async unions breed latent sync assumptions; async-only breaks sync consumer surfaces like extension `find`/`getAll`).
+2. [x] Cloudflare provider auth in pi-ai factories: Workers AI and AI Gateway validate their required account/gateway env/config and return resolved `baseUrl`, provider-scoped env, and header suppression/override metadata from provider auth.
+3. [ ] Add `FileCredentialStore` in coding-agent.
+ - Implement the pi-ai `CredentialStore` interface as a self-contained `auth.json` store; do not depend on the old `AuthStorageBackend` abstraction, though its lock/retry semantics may be ported.
+ - Preserve the existing file format. `ApiKeyCredential` uses `{ type: "api_key", key?, env? }`, matching today's `auth.json`; do not translate `env` into metadata or rewrite discriminators.
+ - Resolve `$ENV`/`!command` in stored API-key `key` and `env` values out of the box using an injected execution/config environment. `$ENV` lookup should come from that environment, and `!command` should run through the shared shell execution path rather than direct `execSync`.
+ - Persist raw config values; resolved credentials returned for auth use must be copies and must not rewrite `$ENV`/`!command` strings unless a caller explicitly stores new values.
+ - `read(provider)` returns the current credential snapshot and records parse/storage errors for status UI parity.
+ - `modify(provider, fn)` must lock, re-read, run `fn`, merge-write the provider entry, chmod `0600`, and return the post-write credential.
+ - `delete(provider)` must lock and remove only that provider's entry.
+ - Add file-backed and in-memory tests covering lock/RMW behavior, `api_key` reads with config-value resolution, OAuth reads, provider `env` preservation, delete, parse errors, and concurrent refresh-style modifications.
+4. [ ] Add runtime override overlay for coding-agent policy.
+ - `withRuntimeOverrides(store, overrides)` implements CLI `--api-key`: read returns an ephemeral `{ type: "api_key", key }` for each overridden provider, masking stored OAuth/API credentials without persisting.
+ - Runtime overrides must apply even to OAuth-capable providers; every provider registered in coding-agent must retain or gain an `apiKey` auth slot so the overlay is meaningful.
+ - Tests cover precedence: runtime override > stored credential > models.json config auth > ambient provider env, with stored credential blocking ambient fallback.
+5. [ ] Build provider decoration helpers for `models.json`.
+ - Start from built-in provider factories, not generated model arrays.
+ - Wrap provider `getModels()` so provider-level `baseUrl`/`headers`/`compat`, per-model `modelOverrides`, and custom model merges apply on every sync read.
+ - Preserve `refreshModels()` passthrough so dynamic providers compose with decorations.
+ - Convert provider `apiKey`/`headers`/`authHeader` models.json config into a wrapped `ApiKeyAuth` that resolves config values first and falls back to the base provider auth.
+ - Custom providers with `models` use `createProvider()` with the appropriate lazy API wrapper or extension-provided stream implementation.
+ - Parse errors must keep current `ModelRegistry.getError()` behavior: built-ins remain available, and the error is visible.
+6. [ ] Copilot `getModels()` baseUrl wrap.
+ - GitHub Copilot OAuth `toAuth()` already returns per-credential request `baseUrl` for streaming.
+ - Wrap Copilot's provider `getModels()` when an OAuth credential is present so extension/UI-visible model metadata also carries the authenticated account base URL.
+ - Keep API-key/env-token Copilot behavior unchanged.
+ - Add tests for model metadata before login, after OAuth credential, after refresh/baseUrl change, and logout.
+7. [ ] Extension OAuth adapter.
+ - Adapt old extension `OAuthProviderInterface` configs to pi-ai `OAuthAuth`.
+ - `login` maps old callbacks/events to `prompt()/notify()`.
+ - `refreshToken` maps to `refresh`.
+ - `getApiKey` maps to `toAuth`.
+ - `modifyModels` becomes a provider `getModels()` wrapper plus `toAuth().baseUrl` where applicable.
+ - Preserve existing extension runtime compatibility through the `/compat` alias until Phase 10.
+8. [ ] Rebuild coding-agent `ModelRegistry` over `MutableModels`.
+ - It owns a `MutableModels` instance built from decorated built-ins + models.json custom providers + extension providers.
+ - `getAll()`, `find()`, and `getAvailable()` remain sync compatibility methods over last-known model lists and fast configured-looking auth status. Do not break the extension-facing `modelRegistry` surface for these reads.
+ - `refresh()` is the explicit async freshness boundary: rebuild provider layers and call `models.refresh()` where needed; no global api-registry reset should be part of the new path except compat-only grace behavior.
+ - `registerProvider()`/`unregisterProvider()` mutate provider layers and rebuild the collection.
+ - Facade auth ops (`login`, `logout`, provider status, available OAuth providers) drive `provider.auth.{apiKey,oauth}` and the `CredentialStore`; no `AuthStorage` type remains.
+ - Legacy `registerApiProvider` writes stay only for `/compat` callers and are removed in Phase 10.
+9. [ ] Rewire consumers.
+ - `AgentSession` stream function resolves through `ModelRegistry`/`Models`, not `getApiKeyAndHeaders()` + compat globals.
+ - SDK options replace `authStorage` with `credentials?: CredentialStore` or an agent-dir-backed default; update `sdk.md` and examples.
+ - `model-resolver`, `--list-models`, model selector, login/logout/status UI, and provider attribution use sync last-known model reads and await only explicit refresh/auth operations.
+ - CLI `--api-key` populates the runtime override decorator instead of mutating `AuthStorage`.
+ - Keep extension loader root-to-compat alias until Phase 10, but expose the new collection/facade as the forward API.
+10. [ ] Test migration and real-provider validation.
+ - Unit tests for `FileCredentialStore`, runtime override overlay, provider decoration, extension OAuth adapter, Models-backed ModelRegistry facade, and consumer rewiring.
+ - Regression tests for Cloudflare account/gateway env, Copilot OAuth baseUrl wrapping, runtime `--api-key` precedence, `$ENV`/`!command` resolution, and stored credential blocking ambient fallback.
+ - Update existing tests for sync last-known `ModelRegistry.getAll/find/getAvailable` plus explicit async refresh behavior.
+ - Run targeted non-e2e suites plus tmux validation of login flows against real providers (Anthropic OAuth/API key, OpenAI Codex OAuth, GitHub Copilot OAuth, Cloudflare AI Gateway, Bedrock if credentials are available).
+
+### Phase 10 — compat deletion (pi 2.0 era, separate)
+
+- [ ] AgentSession -> AgentHarness; the registry facade dies in favor of harness `Models`.
+- [ ] Move ALL internal `/compat` imports to the new API: every package's src, all tests, and the example extensions (examples then demonstrate the new API). Nothing inside the repo may import `/compat` at that point.
+- [ ] Delete `/compat`, `env-api-keys.ts`, the extension-loader root-to-compat alias, the old `pi-ai/oauth` registry and `OAuthProviderInterface` (incl. `usesCallbackServer`), and the compat-local legacy API registry. This is the extension-author breaking release; changelog carries the migration guide.
+
+### Deferred / follow-ups
+
+- [ ] Web OAuth implementations (sitegeist-style) as an alternative `OAuthAuth`.
+- [x] Images API redesign: `ImagesModels`/`ImagesProvider`/`createImagesProvider` mirror the chat-side design (sync reads, explicit refresh, never-reject generation); auth resolution shared with the chat side via the free-standing `resolveProviderAuth()` in `auth/resolve.ts` (which also owns `ModelsError`; both collections pass their store/context as arguments — no resolver object). `openrouterImagesProvider()` factory + `builtinImagesProviders()`/`builtinImagesModels()` in `providers/all`; impl moved to `api/openrouter-images.ts` with a lazy wrapper. The old global image API (registry + `getImageModel*` + `generateImages`) stays on compat; `ImagesProvider` id alias in types.ts renamed to `ImagesProviderId` (mirror of `Provider` -> `ProviderId`).
+
+## Error behavior
+
+`undefined` means not found or not configured. Real failures reject or become stream errors.
+
+```ts
+export type ModelsErrorCode =
+ | "model_source" // provider model refresh failed
+ | "model_validation" // model object invalid
+ | "provider" // unknown provider, dispatch failure
+ | "stream" // stream setup failure
+ | "auth" // auth resolution failure
+ | "oauth"; // oauth login/refresh failure
+```
+
+- `Models.stream()` produces stream errors (error event + error result) for async setup failures; it does not throw after returning the stream.
+- `Models.getModels()` is a sync best-effort read: a provider whose `getModels()` throws yields no models. `Models.refresh(provider)` rejects on that provider's fetch failure; `Models.refresh()` (all providers) is concurrent best-effort. Apps that need a concrete listing failure refresh the single provider.
+- Auth resolution and credential store failures reject loudly (`ModelsError` codes `auth`/`oauth`); silent fallback to a different auth path after a failure risks billing surprises. A stored credential always blocks ambient/env fallback, including after a failed refresh.
+- Status/availability UIs catch `getAuth` rejections and render "needs re-login"; they do not treat rejection as "unconfigured".
diff --git a/packages/agent/package.json b/packages/agent/package.json
index 66ee4847..7190e909 100644
--- a/packages/agent/package.json
+++ b/packages/agent/package.json
@@ -1,6 +1,6 @@
{
"name": "@earendil-works/pi-agent-core",
- "version": "0.79.6",
+ "version": "0.80.2",
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
"type": "module",
"main": "./dist/index.js",
@@ -29,7 +29,7 @@
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
- "@earendil-works/pi-ai": "^0.79.6",
+ "@earendil-works/pi-ai": "^0.80.2",
"ignore": "7.0.5",
"typebox": "1.1.38",
"yaml": "2.9.0"
@@ -53,8 +53,8 @@
},
"devDependencies": {
"@types/node": "24.12.4",
- "@vitest/coverage-v8": "3.2.4",
+ "@vitest/coverage-v8": "4.1.9",
"typescript": "5.9.3",
- "vitest": "3.2.4"
+ "vitest": "4.1.9"
}
}
diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts
index 77bd9568..d93458d0 100644
--- a/packages/agent/src/agent-loop.ts
+++ b/packages/agent/src/agent-loop.ts
@@ -10,7 +10,7 @@ import {
streamSimple,
type ToolResultMessage,
validateToolArguments,
-} from "@earendil-works/pi-ai";
+} from "@earendil-works/pi-ai/compat";
import type {
AgentContext,
AgentEvent,
diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts
index db6684a8..54020435 100644
--- a/packages/agent/src/agent.ts
+++ b/packages/agent/src/agent.ts
@@ -7,7 +7,7 @@ import {
type TextContent,
type ThinkingBudgets,
type Transport,
-} from "@earendil-works/pi-ai";
+} from "@earendil-works/pi-ai/compat";
import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.ts";
import type {
AfterToolCallContext,
diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts
index 96563465..1d09b054 100644
--- a/packages/agent/src/harness/agent-harness.ts
+++ b/packages/agent/src/harness/agent-harness.ts
@@ -1,10 +1,4 @@
-import {
- type AssistantMessage,
- type ImageContent,
- type Model,
- streamSimple,
- type UserMessage,
-} from "@earendil-works/pi-ai";
+import type { AssistantMessage, ImageContent, Model, Models, UserMessage } from "@earendil-works/pi-ai";
import { runAgentLoop } from "../agent-loop.ts";
import type {
AgentContext,
@@ -75,17 +69,6 @@ function cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHar
};
}
-function mergeHeaders(...headers: Array | undefined>): Record | undefined {
- const merged: Record = {};
- let hasHeaders = false;
- for (const entry of headers) {
- if (!entry) continue;
- Object.assign(merged, entry);
- hasHeaders = true;
- }
- return hasHeaders ? merged : undefined;
-}
-
function findDuplicateNames(names: string[]): string[] {
const seen = new Set();
const duplicates = new Set();
@@ -178,6 +161,7 @@ export class AgentHarness<
> {
readonly env: ExecutionEnv;
private session: Session;
+ readonly models: Models;
private phase: AgentHarnessPhase = "idle";
private runAbortController?: AbortController;
private runPromise?: Promise;
@@ -186,7 +170,6 @@ export class AgentHarness<
private thinkingLevel: ThinkingLevel;
private systemPrompt: AgentHarnessOptions["systemPrompt"];
private streamOptions: AgentHarnessStreamOptions;
- private getApiKeyAndHeaders?: AgentHarnessOptions["getApiKeyAndHeaders"];
private resources: AgentHarnessResources;
private tools = new Map();
private activeToolNames: string[];
@@ -200,10 +183,10 @@ export class AgentHarness<
constructor(options: AgentHarnessOptions) {
this.env = options.env;
this.session = options.session;
+ this.models = options.models;
this.resources = options.resources ?? {};
this.streamOptions = cloneStreamOptions(options.streamOptions);
this.systemPrompt = options.systemPrompt;
- this.getApiKeyAndHeaders = options.getApiKeyAndHeaders;
this.validateUniqueNames(
(options.tools ?? []).map((tool) => tool.name),
"Duplicate tool name(s)",
@@ -376,13 +359,9 @@ export class AgentHarness<
private createStreamFn(getTurnState: () => AgentHarnessTurnState): StreamFn {
return async (model, context, streamOptions) => {
const turnState = getTurnState();
- const auth = await this.getApiKeyAndHeaders?.(model);
- const snapshotOptions: AgentHarnessStreamOptions = {
- ...turnState.streamOptions,
- headers: mergeHeaders(turnState.streamOptions.headers, auth?.headers),
- };
+ const snapshotOptions: AgentHarnessStreamOptions = { ...turnState.streamOptions };
const requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions);
- return streamSimple(model, context, {
+ return this.models.streamSimple(model, context, {
cacheRetention: requestOptions.cacheRetention,
headers: requestOptions.headers,
maxRetries: requestOptions.maxRetries,
@@ -401,7 +380,6 @@ export class AgentHarness<
sessionId: turnState.sessionId,
timeoutMs: requestOptions.timeoutMs,
transport: requestOptions.transport,
- apiKey: auth?.apiKey,
});
};
}
@@ -713,8 +691,6 @@ export class AgentHarness<
try {
const model = this.model;
if (!model) throw new AgentHarnessError("invalid_state", "No model set for compaction");
- const auth = await this.getApiKeyAndHeaders?.(model);
- if (!auth) throw new AgentHarnessError("auth", "No auth available for compaction");
const branchEntries = await this.session.getBranch();
const preparationResult = prepareCompaction(branchEntries, DEFAULT_COMPACTION_SETTINGS);
if (!preparationResult.ok) throw preparationResult.error;
@@ -731,15 +707,7 @@ export class AgentHarness<
const provided = hookResult?.compaction;
const compactResult = provided
? { ok: true as const, value: provided }
- : await compact(
- preparation,
- model,
- auth.apiKey,
- auth.headers,
- customInstructions,
- undefined,
- this.thinkingLevel,
- );
+ : await compact(preparation, this.models, model, customInstructions, undefined, this.thinkingLevel);
if (!compactResult.ok) throw compactResult.error;
const result = compactResult.value;
const entryId = await this.session.appendCompaction(
@@ -792,12 +760,9 @@ export class AgentHarness<
if (!summaryText && options?.summarize && entries.length > 0) {
const model = this.model;
if (!model) throw new AgentHarnessError("invalid_state", "No model set for branch summary");
- const auth = await this.getApiKeyAndHeaders?.(model);
- if (!auth) throw new AgentHarnessError("auth", "No auth available for branch summary");
const branchSummary = await generateBranchSummary(entries, {
+ models: this.models,
model,
- apiKey: auth.apiKey,
- headers: auth.headers,
signal: new AbortController().signal,
customInstructions: hookResult?.customInstructions ?? options?.customInstructions,
replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions,
diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts
index c1824ebf..fdf1df49 100644
--- a/packages/agent/src/harness/compaction/branch-summarization.ts
+++ b/packages/agent/src/harness/compaction/branch-summarization.ts
@@ -1,5 +1,5 @@
-import type { Model } from "@earendil-works/pi-ai";
-import { completeSimple } from "@earendil-works/pi-ai";
+import type { Model, Models } from "@earendil-works/pi-ai";
+
import type { AgentMessage } from "../../types.ts";
import {
convertToLlm,
@@ -49,12 +49,10 @@ export interface CollectEntriesResult {
/** Options for generating a branch summary. */
export interface GenerateBranchSummaryOptions {
+ /** Provider collection the summarization request goes through; owns auth resolution. */
+ models: Models;
/** Model used for summarization. */
model: Model;
- /** API key forwarded to the provider. */
- apiKey: string;
- /** Optional request headers forwarded to the provider. */
- headers?: Record;
/** Abort signal for the summarization request. */
signal: AbortSignal;
/** Optional instructions appended to or replacing the default prompt. */
@@ -202,7 +200,7 @@ export async function generateBranchSummary(
entries: SessionTreeEntry[],
options: GenerateBranchSummaryOptions,
): Promise> {
- const { model, apiKey, headers, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options;
+ const { models, model, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options;
const contextWindow = model.contextWindow || 128000;
const tokenBudget = contextWindow - reserveTokens;
@@ -230,10 +228,10 @@ export async function generateBranchSummary(
timestamp: Date.now(),
},
];
- const response = await completeSimple(
+ const response = await models.completeSimple(
model,
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
- { apiKey, headers, signal, maxTokens: 2048 },
+ { signal, maxTokens: 2048 },
);
if (response.stopReason === "aborted") {
return err(new BranchSummaryError("aborted", response.errorMessage || "Branch summary aborted"));
diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts
index dba753d7..2d6d5583 100644
--- a/packages/agent/src/harness/compaction/compaction.ts
+++ b/packages/agent/src/harness/compaction/compaction.ts
@@ -1,5 +1,4 @@
-import type { AssistantMessage, ImageContent, Model, TextContent, Usage } from "@earendil-works/pi-ai";
-import { completeSimple } from "@earendil-works/pi-ai";
+import type { AssistantMessage, ImageContent, Model, Models, TextContent, Usage } from "@earendil-works/pi-ai";
import type { AgentMessage, ThinkingLevel } from "../../types.ts";
import {
convertToLlm,
@@ -122,14 +121,19 @@ export function calculateContextTokens(usage: Usage): number {
function getAssistantUsage(msg: AgentMessage): Usage | undefined {
if (msg.role === "assistant" && "usage" in msg) {
const assistantMsg = msg as AssistantMessage;
- if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
+ if (
+ assistantMsg.stopReason !== "aborted" &&
+ assistantMsg.stopReason !== "error" &&
+ assistantMsg.usage &&
+ calculateContextTokens(assistantMsg.usage) > 0
+ ) {
return assistantMsg.usage;
}
}
return undefined;
}
-/** Return usage from the last successful assistant message in session entries. */
+/** Return usage from the last valid assistant message in session entries. */
export function getLastAssistantUsage(entries: SessionTreeEntry[]): Usage | undefined {
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
@@ -455,10 +459,9 @@ Keep each section concise. Preserve exact file paths, function names, and error
/** Generate or update a conversation summary for compaction. */
export async function generateSummary(
currentMessages: AgentMessage[],
+ models: Models,
model: Model,
reserveTokens: number,
- apiKey: string,
- headers?: Record,
signal?: AbortSignal,
customInstructions?: string,
previousSummary?: string,
@@ -490,10 +493,10 @@ export async function generateSummary(
const completionOptions =
model.reasoning && thinkingLevel && thinkingLevel !== "off"
- ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
- : { maxTokens, signal, apiKey, headers };
+ ? { maxTokens, signal, reasoning: thinkingLevel }
+ : { maxTokens, signal };
- const response = await completeSimple(
+ const response = await models.completeSimple(
model,
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
completionOptions,
@@ -626,9 +629,8 @@ export { serializeConversation } from "./utils.ts";
/** Generate compaction summary data from prepared session history. */
export async function compact(
preparation: CompactionPreparation,
+ models: Models,
model: Model,
- apiKey: string,
- headers?: Record,
customInstructions?: string,
signal?: AbortSignal,
thinkingLevel?: ThinkingLevel,
@@ -655,25 +657,16 @@ export async function compact(
messagesToSummarize.length > 0
? generateSummary(
messagesToSummarize,
+ models,
model,
settings.reserveTokens,
- apiKey,
- headers,
signal,
customInstructions,
previousSummary,
thinkingLevel,
)
: Promise.resolve(ok("No prior history.")),
- generateTurnPrefixSummary(
- turnPrefixMessages,
- model,
- settings.reserveTokens,
- apiKey,
- headers,
- signal,
- thinkingLevel,
- ),
+ generateTurnPrefixSummary(turnPrefixMessages, models, model, settings.reserveTokens, signal, thinkingLevel),
]);
if (!historyResult.ok) return err(historyResult.error);
if (!turnPrefixResult.ok) return err(turnPrefixResult.error);
@@ -681,10 +674,9 @@ export async function compact(
} else {
const summaryResult = await generateSummary(
messagesToSummarize,
+ models,
model,
settings.reserveTokens,
- apiKey,
- headers,
signal,
customInstructions,
previousSummary,
@@ -706,10 +698,9 @@ export async function compact(
}
async function generateTurnPrefixSummary(
messages: AgentMessage[],
+ models: Models,
model: Model,
reserveTokens: number,
- apiKey: string,
- headers?: Record,
signal?: AbortSignal,
thinkingLevel?: ThinkingLevel,
): Promise> {
@@ -728,12 +719,12 @@ async function generateTurnPrefixSummary(
},
];
- const response = await completeSimple(
+ const response = await models.completeSimple(
model,
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
model.reasoning && thinkingLevel && thinkingLevel !== "off"
- ? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
- : { maxTokens, signal, apiKey, headers },
+ ? { maxTokens, signal, reasoning: thinkingLevel }
+ : { maxTokens, signal },
);
if (response.stopReason === "aborted") {
return err(new CompactionError("aborted", response.errorMessage || "Turn prefix summarization aborted"));
diff --git a/packages/agent/src/harness/env/nodejs.ts b/packages/agent/src/harness/env/nodejs.ts
index e56e7aeb..3d929c82 100644
--- a/packages/agent/src/harness/env/nodejs.ts
+++ b/packages/agent/src/harness/env/nodejs.ts
@@ -144,12 +144,25 @@ async function findBashOnPath(): Promise {
return firstMatch && (await pathExists(firstMatch)) ? firstMatch : null;
}
-async function getShellConfig(
- customShellPath?: string,
-): Promise> {
+interface ShellConfig {
+ shell: string;
+ args: string[];
+ commandTransport?: "argv" | "stdin";
+}
+
+function isLegacyWslBashPath(path: string): boolean {
+ const normalized = path.replace(/\//g, "\\").toLowerCase();
+ return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized);
+}
+
+function getBashShellConfig(shell: string): ShellConfig {
+ return isLegacyWslBashPath(shell) ? { shell, args: ["-s"], commandTransport: "stdin" } : { shell, args: ["-c"] };
+}
+
+async function getShellConfig(customShellPath?: string): Promise> {
if (customShellPath) {
if (await pathExists(customShellPath)) {
- return ok({ shell: customShellPath, args: ["-c"] });
+ return ok(getBashShellConfig(customShellPath));
}
return err(new ExecutionError("shell_unavailable", `Custom shell path not found: ${customShellPath}`));
}
@@ -161,22 +174,22 @@ async function getShellConfig(
if (programFilesX86) candidates.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
for (const candidate of candidates) {
if (await pathExists(candidate)) {
- return ok({ shell: candidate, args: ["-c"] });
+ return ok(getBashShellConfig(candidate));
}
}
const bashOnPath = await findBashOnPath();
if (bashOnPath) {
- return ok({ shell: bashOnPath, args: ["-c"] });
+ return ok(getBashShellConfig(bashOnPath));
}
return err(new ExecutionError("shell_unavailable", "No bash shell found"));
}
if (await pathExists("/bin/bash")) {
- return ok({ shell: "/bin/bash", args: ["-c"] });
+ return ok(getBashShellConfig("/bin/bash"));
}
const bashOnPath = await findBashOnPath();
if (bashOnPath) {
- return ok({ shell: bashOnPath, args: ["-c"] });
+ return ok(getBashShellConfig(bashOnPath));
}
return ok({ shell: "sh", args: ["-c"] });
}
@@ -274,13 +287,22 @@ export class NodeExecutionEnv implements ExecutionEnv {
};
try {
- child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], {
- cwd,
- detached: process.platform !== "win32",
- env: getShellEnv(this.shellEnv, options?.env),
- stdio: ["ignore", "pipe", "pipe"],
- windowsHide: true,
- });
+ const commandFromStdin = shellConfig.value.commandTransport === "stdin";
+ child = spawn(
+ shellConfig.value.shell,
+ commandFromStdin ? shellConfig.value.args : [...shellConfig.value.args, command],
+ {
+ cwd,
+ detached: process.platform !== "win32",
+ env: getShellEnv(this.shellEnv, options?.env),
+ stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "pipe"],
+ windowsHide: true,
+ },
+ );
+ if (commandFromStdin) {
+ child.stdin?.on("error", () => {});
+ child.stdin?.end(command);
+ }
} catch (error) {
const cause = toError(error);
settle(err(new ExecutionError("spawn_error", cause.message, cause)));
diff --git a/packages/agent/src/harness/session/session.ts b/packages/agent/src/harness/session/session.ts
index 6f136208..ce369dff 100644
--- a/packages/agent/src/harness/session/session.ts
+++ b/packages/agent/src/harness/session/session.ts
@@ -234,12 +234,13 @@ export class Session {
}
async appendSessionName(name: string): Promise {
+ const sanitizedName = name.replace(/[\r\n]+/g, " ").trim();
return this.appendTypedEntry({
type: "session_info",
id: await this.storage.createEntryId(),
parentId: await this.storage.getLeafId(),
timestamp: new Date().toISOString(),
- name: name.trim(),
+ name: sanitizedName,
} satisfies SessionInfoEntry);
}
diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts
index 4756ca84..29048e61 100644
--- a/packages/agent/src/harness/types.ts
+++ b/packages/agent/src/harness/types.ts
@@ -1,4 +1,4 @@
-import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai";
+import type { ImageContent, Model, Models, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai";
import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.ts";
import type { Session } from "./session/session.ts";
@@ -240,22 +240,6 @@ export interface FileInfo {
mtimeMs: number;
}
-/** Options for {@link Shell.exec}. */
-export interface ExecutionEnvExecOptions {
- /** Working directory for the command. Relative paths are resolved against {@link ExecutionEnv.cwd}. Defaults to {@link ExecutionEnv.cwd}. */
- cwd?: string;
- /** Additional environment variables for the command. Values override the environment defaults. Defaults to no overrides. */
- env?: Record;
- /** Timeout in seconds. Implementations should return a timeout error when the command exceeds this duration. Defaults to no timeout. */
- timeout?: number;
- /** Abort signal used to terminate the command. Defaults to no abort signal. */
- abortSignal?: AbortSignal;
- /** Called with stdout chunks as they are produced. */
- onStdout?: (chunk: string) => void;
- /** Called with stderr chunks as they are produced. */
- onStderr?: (chunk: string) => void;
-}
-
/**
* Filesystem capability used by the harness.
*
@@ -317,12 +301,28 @@ export interface FileSystem {
cleanup(): Promise;
}
+/** Options for {@link Shell.exec}. */
+export interface ShellExecOptions {
+ /** Working directory for the command. Relative paths are resolved against {@link ExecutionEnv.cwd}. Defaults to {@link ExecutionEnv.cwd}. */
+ cwd?: string;
+ /** Additional environment variables for the command. Values override the environment defaults. Defaults to no overrides. */
+ env?: Record;
+ /** Timeout in seconds. Implementations should return a timeout error when the command exceeds this duration. Defaults to no timeout. */
+ timeout?: number;
+ /** Abort signal used to terminate the command. Defaults to no abort signal. */
+ abortSignal?: AbortSignal;
+ /** Called with stdout chunks as they are produced. */
+ onStdout?: (chunk: string) => void;
+ /** Called with stderr chunks as they are produced. */
+ onStderr?: (chunk: string) => void;
+}
+
/** Shell execution capability used by the harness. */
export interface Shell {
/** Execute a shell command in {@link FileSystem.cwd} unless `options.cwd` is provided. */
exec(
command: string,
- options?: ExecutionEnvExecOptions,
+ options?: ShellExecOptions,
): Promise>;
/** Release shell resources. Must be best-effort and must not throw or reject. */
cleanup(): Promise;
@@ -802,6 +802,12 @@ export interface AgentHarnessOptions<
> {
env: ExecutionEnv;
session: Session;
+ /**
+ * Provider collection used for all model requests (turn streaming,
+ * compaction, branch summarization). Auth resolves through the providers'
+ * auth.
+ */
+ models: Models;
tools?: TTool[];
/**
* Concrete resources available to explicit invocation methods and system-prompt callbacks.
@@ -818,9 +824,6 @@ export interface AgentHarnessOptions<
activeTools: TTool[];
resources: AgentHarnessResources;
}) => string | Promise);
- getApiKeyAndHeaders?: (
- model: Model,
- ) => Promise<{ apiKey: string; headers?: Record } | undefined>;
/** Curated stream/provider request options. Snapshotted at turn start. */
streamOptions?: AgentHarnessStreamOptions;
model: Model;
diff --git a/packages/agent/src/harness/utils/shell-output.ts b/packages/agent/src/harness/utils/shell-output.ts
index fea46d95..a98b51a5 100644
--- a/packages/agent/src/harness/utils/shell-output.ts
+++ b/packages/agent/src/harness/utils/shell-output.ts
@@ -1,15 +1,7 @@
-import {
- type ExecutionEnv,
- type ExecutionEnvExecOptions,
- ExecutionError,
- err,
- ok,
- type Result,
- toError,
-} from "../types.ts";
+import { type ExecutionEnv, ExecutionError, err, ok, type Result, type ShellExecOptions, toError } from "../types.ts";
import { DEFAULT_MAX_BYTES, truncateTail } from "./truncate.ts";
-export interface ShellCaptureOptions extends Omit {
+export interface ShellCaptureOptions extends Omit {
onChunk?: (chunk: string) => void;
}
diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts
index cb99a79a..abfa3de6 100644
--- a/packages/agent/src/types.ts
+++ b/packages/agent/src/types.ts
@@ -1,11 +1,13 @@
import type {
+ Api,
AssistantMessage,
AssistantMessageEvent,
+ AssistantMessageEventStream,
+ Context,
ImageContent,
Message,
Model,
SimpleStreamOptions,
- streamSimple,
TextContent,
Tool,
ToolResultMessage,
@@ -13,7 +15,8 @@ import type {
import type { Static, TSchema } from "typebox";
/**
- * Stream function used by the agent loop.
+ * Stream function used by the agent loop. `Models.streamSimple` satisfies
+ * this shape.
*
* Contract:
* - Must not throw or return a rejected promise for request/model/runtime failures.
@@ -22,8 +25,10 @@ import type { Static, TSchema } from "typebox";
* final AssistantMessage with stopReason "error" or "aborted" and errorMessage.
*/
export type StreamFn = (
- ...args: Parameters
-) => ReturnType | Promise>;
+ model: Model,
+ context: Context,
+ options?: SimpleStreamOptions,
+) => AssistantMessageEventStream | Promise;
/**
* Configuration for how tool calls from a single assistant message are executed.
diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts
index 4cc51f74..5fa27c5a 100644
--- a/packages/agent/test/agent.test.ts
+++ b/packages/agent/test/agent.test.ts
@@ -1,4 +1,4 @@
-import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai";
+import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai/compat";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { Agent, type AgentEvent, type AgentTool, type AgentToolUpdateCallback } from "../src/index.ts";
diff --git a/packages/agent/test/e2e.test.ts b/packages/agent/test/e2e.test.ts
index 309536ad..57d70585 100644
--- a/packages/agent/test/e2e.test.ts
+++ b/packages/agent/test/e2e.test.ts
@@ -9,7 +9,7 @@ import {
registerFauxProvider,
type ToolResultMessage,
type UserMessage,
-} from "@earendil-works/pi-ai";
+} from "@earendil-works/pi-ai/compat";
import { afterEach, describe, expect, it } from "vitest";
import { Agent, type AgentEvent } from "../src/index.ts";
import { calculateTool } from "./utils/calculate.ts";
diff --git a/packages/agent/test/harness/agent-harness-stream.test.ts b/packages/agent/test/harness/agent-harness-stream.test.ts
index ee79564b..f5a4021d 100644
--- a/packages/agent/test/harness/agent-harness-stream.test.ts
+++ b/packages/agent/test/harness/agent-harness-stream.test.ts
@@ -1,18 +1,27 @@
-import { fauxAssistantMessage, fauxToolCall, registerFauxProvider, type StreamOptions } from "@earendil-works/pi-ai";
-import { afterEach, describe, expect, it } from "vitest";
+import {
+ createModels,
+ type FauxProviderHandle,
+ fauxAssistantMessage,
+ fauxProvider,
+ fauxToolCall,
+ type StreamOptions,
+} from "@earendil-works/pi-ai";
+import { describe, expect, it } from "vitest";
import { AgentHarness } from "../../src/harness/agent-harness.ts";
import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts";
import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts";
import { Session } from "../../src/harness/session/session.ts";
import { calculateTool } from "../utils/calculate.ts";
-const registrations: Array<{ unregister(): void }> = [];
+/** Shared collection; each faux provider gets a unique id so coexisting fakes route correctly. */
+const models = createModels();
+let fauxCount = 0;
-afterEach(() => {
- for (const registration of registrations.splice(0)) {
- registration.unregister();
- }
-});
+function newFaux(): FauxProviderHandle {
+ const faux = fauxProvider({ provider: `faux-${++fauxCount}` });
+ models.setProvider(faux.provider);
+ return faux;
+}
function createHarness(options: ConstructorParameters[0]): AgentHarness {
return new AgentHarness(options);
@@ -27,10 +36,9 @@ function captureOptions(options: StreamOptions | undefined): StreamOptions {
}
describe("AgentHarness stream configuration", () => {
- it("snapshots stream options and merges auth headers before provider request hooks", async () => {
+ it("snapshots stream options before provider request hooks", async () => {
let capturedOptions: StreamOptions | undefined;
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
registration.setResponses([
(_context, options) => {
capturedOptions = options;
@@ -40,6 +48,7 @@ describe("AgentHarness stream configuration", () => {
const session = new Session(new InMemorySessionStorage({ metadata: { id: "session-1", createdAt: "now" } }));
const harness = createHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session,
model: registration.getModel(),
@@ -51,12 +60,11 @@ describe("AgentHarness stream configuration", () => {
metadata: { base: true },
cacheRetention: "none",
},
- getApiKeyAndHeaders: async () => ({ apiKey: "secret", headers: { "x-auth": "auth" } }),
});
harness.on("before_provider_request", (event) => {
expect(event.sessionId).toBe("session-1");
- expect(event.streamOptions.headers).toEqual({ "x-base": "base", "x-auth": "auth" });
+ expect(event.streamOptions.headers).toEqual({ "x-base": "base" });
return {
streamOptions: {
headers: { "x-hook": "hook" },
@@ -68,21 +76,19 @@ describe("AgentHarness stream configuration", () => {
await harness.prompt("hello");
expect(capturedOptions).toMatchObject({
- apiKey: "secret",
timeoutMs: 1000,
maxRetries: 2,
maxRetryDelayMs: 3000,
sessionId: "session-1",
cacheRetention: "none",
});
- expect(capturedOptions?.headers).toEqual({ "x-base": "base", "x-auth": "auth", "x-hook": "hook" });
+ expect(capturedOptions?.headers).toEqual({ "x-base": "base", "x-hook": "hook" });
expect(capturedOptions?.metadata).toEqual({ base: true, hook: true });
});
it("chains provider request patches and supports deletion semantics", async () => {
let capturedOptions: StreamOptions | undefined;
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
registration.setResponses([
(_context, options) => {
capturedOptions = options;
@@ -91,6 +97,7 @@ describe("AgentHarness stream configuration", () => {
]);
const harness = createHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session: new Session(new InMemorySessionStorage()),
model: registration.getModel(),
@@ -133,8 +140,7 @@ describe("AgentHarness stream configuration", () => {
it("uses updated stream options for save-point snapshots without mutating the active request", async () => {
const capturedOptions: StreamOptions[] = [];
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
registration.setResponses([
(_context, options) => {
capturedOptions.push(captureOptions(options));
@@ -149,6 +155,7 @@ describe("AgentHarness stream configuration", () => {
]);
const harness = createHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session: new Session(new InMemorySessionStorage()),
model: registration.getModel(),
@@ -174,8 +181,7 @@ describe("AgentHarness stream configuration", () => {
it("chains provider payload hooks", async () => {
const seenPayloads: unknown[] = [];
let finalPayload: unknown;
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
registration.setResponses([
async (_context, options, _state, model) => {
finalPayload = await options?.onPayload?.({ steps: ["provider"] }, model);
@@ -184,6 +190,7 @@ describe("AgentHarness stream configuration", () => {
]);
const harness = createHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session: new Session(new InMemorySessionStorage()),
model: registration.getModel(),
diff --git a/packages/agent/test/harness/agent-harness.test.ts b/packages/agent/test/harness/agent-harness.test.ts
index 1d24eb4c..d13eca84 100644
--- a/packages/agent/test/harness/agent-harness.test.ts
+++ b/packages/agent/test/harness/agent-harness.test.ts
@@ -1,5 +1,13 @@
-import { fauxAssistantMessage, fauxToolCall, getModel, registerFauxProvider } from "@earendil-works/pi-ai";
-import { afterEach, describe, expect, it } from "vitest";
+import {
+ createModels,
+ type FauxProviderHandle,
+ fauxAssistantMessage,
+ fauxProvider,
+ fauxToolCall,
+ type RegisterFauxProviderOptions,
+} from "@earendil-works/pi-ai";
+import { getModel } from "@earendil-works/pi-ai/compat";
+import { describe, expect, it } from "vitest";
import { AgentHarness } from "../../src/harness/agent-harness.ts";
import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts";
import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts";
@@ -17,7 +25,15 @@ interface AppPromptTemplate extends PromptTemplate {
source: "project" | "user";
}
-const registrations: Array<{ unregister(): void }> = [];
+/** Shared collection; each faux provider gets a unique id so coexisting fakes route correctly. */
+const models = createModels();
+let fauxCount = 0;
+
+function newFaux(options: RegisterFauxProviderOptions = {}): FauxProviderHandle {
+ const faux = fauxProvider({ provider: `faux-${++fauxCount}`, ...options });
+ models.setProvider(faux.provider);
+ return faux;
+}
function textFromUserMessages(messages: Array<{ role: string; content: unknown }>): string[] {
return messages.flatMap((message) => {
@@ -44,18 +60,13 @@ function getReasoning(options: unknown): unknown {
return options.reasoning;
}
-afterEach(() => {
- for (const registration of registrations.splice(0)) {
- registration.unregister();
- }
-});
-
describe("AgentHarness", () => {
it("constructs directly and exposes queue modes", () => {
const session = new Session(new InMemorySessionStorage());
const env = new NodeExecutionEnv({ cwd: process.cwd() });
const initialModel = getModel("anthropic", "claude-sonnet-4-5");
const harness = new AgentHarness({
+ models,
env,
session,
model: initialModel,
@@ -76,8 +87,7 @@ describe("AgentHarness", () => {
});
it("drains one queued steering message at a time and emits queue updates", async () => {
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
const userCounts: number[] = [];
registration.setResponses([
(context) => {
@@ -94,6 +104,7 @@ describe("AgentHarness", () => {
},
]);
const harness = new AgentHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session: new Session(new InMemorySessionStorage()),
model: registration.getModel(),
@@ -119,8 +130,7 @@ describe("AgentHarness", () => {
});
it("appends before_agent_start messages and persists them", async () => {
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
let requestText: string[] = [];
registration.setResponses([
(context) => {
@@ -130,6 +140,7 @@ describe("AgentHarness", () => {
]);
const session = new Session(new InMemorySessionStorage());
const harness = new AgentHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session,
model: registration.getModel(),
@@ -151,8 +162,7 @@ describe("AgentHarness", () => {
});
it("abort clears steer and follow-up queues but preserves next-turn messages", async () => {
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
let releaseFirstResponse: (() => void) | undefined;
let abortedSignal: AbortSignal | undefined;
const firstResponseReleased = new Promise((resolve) => {
@@ -171,6 +181,7 @@ describe("AgentHarness", () => {
},
]);
const harness = new AgentHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session: new Session(new InMemorySessionStorage()),
model: registration.getModel(),
@@ -206,8 +217,7 @@ describe("AgentHarness", () => {
});
it("drains follow-up messages one at a time after the agent would otherwise stop", async () => {
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
const userCounts: number[] = [];
registration.setResponses([
(context) => {
@@ -224,6 +234,7 @@ describe("AgentHarness", () => {
},
]);
const harness = new AgentHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session: new Session(new InMemorySessionStorage()),
model: registration.getModel(),
@@ -249,11 +260,11 @@ describe("AgentHarness", () => {
});
it("settles thrown hook failures with persisted assistant error messages", async () => {
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
registration.setResponses([() => fauxAssistantMessage("should not be used")]);
const session = new Session(new InMemorySessionStorage());
const harness = new AgentHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session,
model: registration.getModel(),
@@ -280,13 +291,12 @@ describe("AgentHarness", () => {
});
it("refreshes model, thinking level, resources, system prompt, and active tools at save points", async () => {
- const registration = registerFauxProvider({
+ const registration = newFaux({
models: [
{ id: "first", reasoning: true },
{ id: "second", reasoning: true },
],
});
- registrations.push(registration);
const secondModel = registration.getModel("second");
if (!secondModel) throw new Error("missing second faux model");
const captured: Array<{ modelId: string; reasoning: unknown; systemPrompt: string; tools: string[] }> = [];
@@ -313,6 +323,7 @@ describe("AgentHarness", () => {
},
]);
const harness = new AgentHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session: new Session(new InMemorySessionStorage()),
model: registration.getModel(),
@@ -345,11 +356,11 @@ describe("AgentHarness", () => {
});
it("orders pending listener session writes after agent-emitted messages", async () => {
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
registration.setResponses([() => fauxAssistantMessage("ok")]);
const session = new Session(new InMemorySessionStorage());
const harness = new AgentHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session,
model: registration.getModel(),
@@ -376,11 +387,11 @@ describe("AgentHarness", () => {
});
it("waitForIdle waits for external run settlement and awaited listeners", async () => {
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
registration.setResponses([() => fauxAssistantMessage("ok")]);
const barrier = deferred();
const harness = new AgentHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session: new Session(new InMemorySessionStorage()),
model: registration.getModel(),
@@ -408,8 +419,7 @@ describe("AgentHarness", () => {
});
it("runs tool_call and tool_result hooks through the direct loop", async () => {
- const registration = registerFauxProvider();
- registrations.push(registration);
+ const registration = newFaux();
registration.setResponses([
() =>
fauxAssistantMessage(fauxToolCall("calculate", { expression: "2 + 2" }, { id: "call-1" }), {
@@ -418,6 +428,7 @@ describe("AgentHarness", () => {
]);
const session = new Session(new InMemorySessionStorage());
const harness = new AgentHarness({
+ models,
env: new NodeExecutionEnv({ cwd: process.cwd() }),
session,
model: registration.getModel(),
@@ -462,6 +473,7 @@ describe("AgentHarness", () => {
const inspectTool: AppTool = { ...calculateTool, name: "inspect", source: "builtin" };
const searchTool: AppTool = { ...calculateTool, name: "search", source: "extension" };
const harness = new AgentHarness({
+ models,
env,
session,
model,
@@ -530,11 +542,12 @@ describe("AgentHarness", () => {
const env = new NodeExecutionEnv({ cwd: process.cwd() });
const model = getModel("anthropic", "claude-sonnet-4-5");
expect(
- () => new AgentHarness({ env, session, model, tools: [calculateTool], activeToolNames: ["missing"] }),
+ () => new AgentHarness({ env, session, models, model, tools: [calculateTool], activeToolNames: ["missing"] }),
).toThrow(/Unknown tool/);
expect(
() =>
new AgentHarness({
+ models,
env,
session,
model,
@@ -545,6 +558,7 @@ describe("AgentHarness", () => {
expect(
() =>
new AgentHarness({
+ models,
env,
session,
model,
@@ -558,7 +572,7 @@ describe("AgentHarness", () => {
const session = new Session(new InMemorySessionStorage());
const env = new NodeExecutionEnv({ cwd: process.cwd() });
const model = getModel("anthropic", "claude-sonnet-4-5");
- const harness = new AgentHarness({ env, session, model });
+ const harness = new AgentHarness({ env, session, models, model });
const skill: AppSkill = {
name: "inspect",
description: "Inspect things",
diff --git a/packages/agent/test/harness/compaction.test.ts b/packages/agent/test/harness/compaction.test.ts
index b694d9f5..8a228eeb 100644
--- a/packages/agent/test/harness/compaction.test.ts
+++ b/packages/agent/test/harness/compaction.test.ts
@@ -1,13 +1,14 @@
import {
type AssistantMessage,
- type FauxProviderRegistration,
+ createModels,
+ type FauxProviderHandle,
fauxAssistantMessage,
+ fauxProvider,
type Message,
type Model,
- registerFauxProvider,
type Usage,
} from "@earendil-works/pi-ai";
-import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { beforeEach, describe, expect, it } from "vitest";
import {
type CompactionPreparation,
calculateContextTokens,
@@ -121,11 +122,13 @@ function createModelChangeEntry(provider: string, modelId: string, parentId: str
};
}
-function createFauxModel(
- reasoning: boolean,
- maxTokens = 8192,
-): { faux: FauxProviderRegistration; model: Model } {
- const faux = registerFauxProvider({
+/** Shared collection; each faux provider gets a unique id so coexisting fakes route correctly. */
+const models = createModels();
+let fauxCount = 0;
+
+function createFauxModel(reasoning: boolean, maxTokens = 8192): { faux: FauxProviderHandle; model: Model } {
+ const faux = fauxProvider({
+ provider: `faux-${++fauxCount}`,
models: [
{
id: reasoning ? "reasoning-model" : "non-reasoning-model",
@@ -135,18 +138,10 @@ function createFauxModel(
},
],
});
- fauxRegistrations.push(faux);
+ models.setProvider(faux.provider);
return { faux, model: faux.getModel() };
}
-const fauxRegistrations: FauxProviderRegistration[] = [];
-
-afterEach(() => {
- while (fauxRegistrations.length > 0) {
- fauxRegistrations.pop()?.unregister();
- }
-});
-
describe("harness compaction", () => {
beforeEach(() => {
nextId = 0;
@@ -306,11 +301,28 @@ describe("harness compaction", () => {
createMessageEntry({ ...assistant, stopReason: "error" }),
]),
).toBeUndefined();
+ expect(
+ getLastAssistantUsage([
+ createMessageEntry(createUserMessage("user")),
+ createMessageEntry(assistant),
+ createMessageEntry(createAssistantMessage("partial", createMockUsage(0, 0))),
+ ]),
+ ).toBe(usage);
expect(estimateContextTokens([createUserMessage("no usage")]).lastUsageIndex).toBeNull();
expect(estimateContextTokens([assistant, createUserMessage("tail")])).toMatchObject({
usageTokens: 20,
lastUsageIndex: 0,
});
+ const estimate = estimateContextTokens([
+ createUserMessage("Hello"),
+ assistant,
+ createUserMessage("continue"),
+ createAssistantMessage("Partial thinking", createMockUsage(0, 0)),
+ ]);
+ expect(estimate.usageTokens).toBe(20);
+ expect(estimate.lastUsageIndex).toBe(1);
+ expect(estimate.trailingTokens).toBeGreaterThan(0);
+ expect(estimate.tokens).toBe(20 + estimate.trailingTokens);
});
it("builds session context with a compaction entry", () => {
@@ -445,19 +457,9 @@ describe("harness compaction", () => {
},
]);
getOrThrow(
- await generateSummary(
- messages,
- reasoningModel,
- 2000,
- "test-key",
- undefined,
- undefined,
- undefined,
- undefined,
- "medium",
- ),
+ await generateSummary(messages, models, reasoningModel, 2000, undefined, undefined, undefined, "medium"),
);
- expect(seenOptions[0]).toMatchObject({ reasoning: "medium", apiKey: "test-key" });
+ expect(seenOptions[0]).toMatchObject({ reasoning: "medium" });
const { faux: fauxOff, model: offModel } = createFauxModel(true);
fauxOff.setResponses([
@@ -466,9 +468,7 @@ describe("harness compaction", () => {
return fauxAssistantMessage("## Goal\nTest summary");
},
]);
- getOrThrow(
- await generateSummary(messages, offModel, 2000, "test-key", undefined, undefined, undefined, undefined, "off"),
- );
+ getOrThrow(await generateSummary(messages, models, offModel, 2000, undefined, undefined, undefined, "off"));
expect(seenOptions[1]).not.toHaveProperty("reasoning");
const { faux: fauxNonReasoning, model: nonReasoningModel } = createFauxModel(false);
@@ -479,17 +479,7 @@ describe("harness compaction", () => {
},
]);
getOrThrow(
- await generateSummary(
- messages,
- nonReasoningModel,
- 2000,
- "test-key",
- undefined,
- undefined,
- undefined,
- undefined,
- "medium",
- ),
+ await generateSummary(messages, models, nonReasoningModel, 2000, undefined, undefined, undefined, "medium"),
);
expect(seenOptions[2]).not.toHaveProperty("reasoning");
});
@@ -508,16 +498,7 @@ describe("harness compaction", () => {
]);
const summary = getOrThrow(
- await generateSummary(
- messages,
- model,
- 2000,
- "test-key",
- { "x-test": "yes" },
- undefined,
- "focus",
- "old summary",
- ),
+ await generateSummary(messages, models, model, 2000, undefined, "focus", "old summary"),
);
expect(summary).toContain("Test summary");
@@ -529,7 +510,7 @@ describe("harness compaction", () => {
const messages: AgentMessage[] = [createUserMessage("Summarize this.")];
const { faux: errorFaux, model: errorModel } = createFauxModel(false);
errorFaux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "boom" })]);
- const errorResult = await generateSummary(messages, errorModel, 2000, "test-key");
+ const errorResult = await generateSummary(messages, models, errorModel, 2000);
expect(errorResult).toMatchObject({
ok: false,
error: { code: "summarization_failed", message: "Summarization failed: boom" },
@@ -537,7 +518,7 @@ describe("harness compaction", () => {
const { faux: abortedFaux, model: abortedModel } = createFauxModel(false);
abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "stopped" })]);
- const abortedResult = await generateSummary(messages, abortedModel, 2000, "test-key");
+ const abortedResult = await generateSummary(messages, models, abortedModel, 2000);
expect(abortedResult).toMatchObject({ ok: false, error: { code: "aborted", message: "stopped" } });
});
@@ -565,7 +546,7 @@ describe("harness compaction", () => {
settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 },
};
- getOrThrow(await compact(preparation, model, "test-key"));
+ getOrThrow(await compact(preparation, models, model));
expect(seenOptions.map((options) => options?.maxTokens)).toEqual([128000, 128000]);
});
@@ -583,7 +564,7 @@ describe("harness compaction", () => {
};
const { faux: historyFaux, model: historyModel } = createFauxModel(false);
historyFaux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "history failed" })]);
- expect(await compact(preparation, historyModel, "test-key")).toMatchObject({
+ expect(await compact(preparation, models, historyModel)).toMatchObject({
ok: false,
error: { code: "summarization_failed", message: "Summarization failed: history failed" },
});
@@ -591,8 +572,8 @@ describe("harness compaction", () => {
const { model: invalidModel } = createFauxModel(false);
const invalidResult = await compact(
{ ...preparation, messagesToSummarize: [], firstKeptEntryId: "" },
+ models,
invalidModel,
- "test-key",
);
expect(invalidResult).toMatchObject({ ok: false, error: { code: "invalid_session" } });
});
@@ -617,7 +598,7 @@ describe("harness compaction", () => {
settings: { enabled: true, reserveTokens: 2000, keepRecentTokens: 20 },
};
- getOrThrow(await compact(preparation, model, "test-key", undefined, undefined, undefined, "high"));
+ getOrThrow(await compact(preparation, models, model, undefined, undefined, "high"));
expect(seenOptions[0]).toMatchObject({ reasoning: "high" });
});
@@ -636,14 +617,14 @@ describe("harness compaction", () => {
const { faux, model } = createFauxModel(false);
faux.setResponses([fauxAssistantMessage("", { stopReason: "error", errorMessage: "prefix failed" })]);
- expect(await compact(preparation, model, "test-key")).toMatchObject({
+ expect(await compact(preparation, models, model)).toMatchObject({
ok: false,
error: { code: "summarization_failed", message: "Turn prefix summarization failed: prefix failed" },
});
const { faux: abortedFaux, model: abortedModel } = createFauxModel(false);
abortedFaux.setResponses([fauxAssistantMessage("", { stopReason: "aborted", errorMessage: "prefix stopped" })]);
- expect(await compact(preparation, abortedModel, "test-key")).toMatchObject({
+ expect(await compact(preparation, models, abortedModel)).toMatchObject({
ok: false,
error: { code: "aborted", message: "prefix stopped" },
});
@@ -662,7 +643,7 @@ describe("harness compaction", () => {
expect(preparation).toBeDefined();
const { faux, model } = createFauxModel(false);
faux.setResponses([fauxAssistantMessage("## Goal\nTest summary")]);
- const result = getOrThrow(await compact(preparation!, model, "test-key"));
+ const result = getOrThrow(await compact(preparation!, models, model));
expect(result.summary.length).toBeGreaterThan(0);
expect(result.firstKeptEntryId).toBeTruthy();
expect(result.details).toBeDefined();
diff --git a/packages/agent/test/harness/nodejs-env.test.ts b/packages/agent/test/harness/nodejs-env.test.ts
index 758d5f59..d2d33a6f 100644
--- a/packages/agent/test/harness/nodejs-env.test.ts
+++ b/packages/agent/test/harness/nodejs-env.test.ts
@@ -1,5 +1,5 @@
import { access, chmod, realpath, symlink } from "node:fs/promises";
-import { join } from "node:path";
+import { delimiter, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts";
import { FileError, getOrThrow } from "../../src/harness/types.ts";
@@ -201,6 +201,39 @@ describe("NodeExecutionEnv", () => {
expect(result).toEqual({ stdout: `${await realpath(root)}:ok`, stderr: "", exitCode: 0 });
});
+ it("uses stdin command transport for legacy WSL bash paths", async () => {
+ if (process.platform === "win32") return;
+ const root = createTempDir();
+ const shellPath = "C:\\Windows\\System32\\bash.exe";
+ const env = new NodeExecutionEnv({ cwd: root });
+ getOrThrow(await env.writeFile(shellPath, '#!/bin/sh\nprintf \'args:%s\\n\' "$*" >&2\nexec /bin/bash "$@"\n'));
+ await chmod(join(root, shellPath), 0o755);
+
+ const originalCwd = process.cwd();
+ const originalPath = process.env.PATH;
+ const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
+ try {
+ process.chdir(root);
+ process.env.PATH = `${root}${delimiter}${originalPath ?? ""}`;
+ Object.defineProperty(process, "platform", {
+ configurable: true,
+ value: "win32",
+ });
+
+ const wslEnv = new NodeExecutionEnv({ cwd: root, shellPath });
+ const nameExpansion = "$" + "{name}";
+ const result = getOrThrow(await wslEnv.exec(`name='World'; echo "Hello, ${nameExpansion}!"`));
+
+ expect(result).toEqual({ stdout: "Hello, World!\n", stderr: "args:-s\n", exitCode: 0 });
+ } finally {
+ process.chdir(originalCwd);
+ process.env.PATH = originalPath;
+ if (platformDescriptor) {
+ Object.defineProperty(process, "platform", platformDescriptor);
+ }
+ }
+ });
+
it("streams stdout and stderr chunks", async () => {
const root = createTempDir();
const env = new NodeExecutionEnv({ cwd: root });
diff --git a/packages/agent/test/harness/session.test.ts b/packages/agent/test/harness/session.test.ts
index c9598da8..39e285b5 100644
--- a/packages/agent/test/harness/session.test.ts
+++ b/packages/agent/test/harness/session.test.ts
@@ -86,6 +86,12 @@ async function runSessionSuite(
expect(context.messages[1]?.role).toBe("custom");
});
+ it("normalizes session names", async () => {
+ const session = new Session(await createStorage());
+ await session.appendSessionName(" hello\nworld\r\nagain ");
+ expect(await session.getSessionName()).toBe("hello world again");
+ });
+
it("supports labels and session info entries without affecting context", async () => {
const session = new Session(await createStorage());
const user1 = await session.appendMessage(createUserMessage("one"));
diff --git a/packages/agent/test/scratch/simple.ts b/packages/agent/test/scratch/simple.ts
index de6ba3a2..b4feb665 100644
--- a/packages/agent/test/scratch/simple.ts
+++ b/packages/agent/test/scratch/simple.ts
@@ -1,6 +1,8 @@
import { homedir } from "node:os";
import { join } from "node:path";
-import { getModel } from "@earendil-works/pi-ai";
+import { createModels } from "@earendil-works/pi-ai";
+import { cloudflareAIGatewayProvider } from "@earendil-works/pi-ai/providers/cloudflare-ai-gateway";
+import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts";
import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts";
import {
@@ -35,11 +37,22 @@ const { promptTemplates: sourcedPromptTemplates } = await loadSourcedPromptTempl
(promptTemplate, source) => ({ ...promptTemplate, source }),
);
+const models = createModels();
+models.setProvider(openaiProvider());
+models.setProvider(cloudflareAIGatewayProvider());
+const model = models.getModel("openai", "gpt-5.5");
+// const model = models.getModel("cloudflare-ai-gateway", "claude-haiku-4-5");
+if (!model) {
+ console.log("Model not found");
+ process.exit(-1);
+}
+
const session = new Session(new InMemorySessionStorage());
const agent = new AgentHarness({
env,
session,
- model: getModel("openai", "gpt-5.5"),
+ models,
+ model,
thinkingLevel: "low",
systemPrompt: ({ env, resources }) =>
[
diff --git a/packages/agent/vitest.config.ts b/packages/agent/vitest.config.ts
index bcc497fa..b0f0bb43 100644
--- a/packages/agent/vitest.config.ts
+++ b/packages/agent/vitest.config.ts
@@ -1,9 +1,19 @@
+import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
+const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url));
+const aiSrcCompat = fileURLToPath(new URL("../ai/src/compat.ts", import.meta.url));
+
export default defineConfig({
test: {
globals: true,
environment: "node",
testTimeout: 30000, // 30 seconds for API calls
},
+ resolve: {
+ alias: [
+ { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex },
+ { find: /^@earendil-works\/pi-ai\/compat$/, replacement: aiSrcCompat },
+ ],
+ },
});
diff --git a/packages/agent/vitest.harness.config.ts b/packages/agent/vitest.harness.config.ts
index 9421e5a9..91c0d471 100644
--- a/packages/agent/vitest.harness.config.ts
+++ b/packages/agent/vitest.harness.config.ts
@@ -1,5 +1,9 @@
+import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
+const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url));
+const aiSrcCompat = fileURLToPath(new URL("../ai/src/compat.ts", import.meta.url));
+
export default defineConfig({
test: {
globals: true,
@@ -15,4 +19,10 @@ export default defineConfig({
reportsDirectory: "coverage/harness",
},
},
+ resolve: {
+ alias: [
+ { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex },
+ { find: /^@earendil-works\/pi-ai\/compat$/, replacement: aiSrcCompat },
+ ],
+ },
});
diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md
index a49ddf52..03818dc3 100644
--- a/packages/ai/CHANGELOG.md
+++ b/packages/ai/CHANGELOG.md
@@ -2,6 +2,133 @@
## [Unreleased]
+### Fixed
+
+- Fixed retry classification for provider errors that explicitly tell callers to retry the request ([#6019](https://github.com/earendil-works/pi/issues/6019)).
+
+## [0.80.2] - 2026-06-23
+
+### Changed
+
+- Changed `ApiKeyCredential` to use the `auth.json`-compatible discriminator `type: "api_key"` and provider-scoped `env` values instead of `type: "api-key"` and metadata.
+
+### Fixed
+
+- Fixed Anthropic-compatible custom models to use explicit compatibility metadata instead of provider-name heuristics for session-affinity headers and unsupported tool-field omissions.
+- Fixed request-scoped `apiKey` and `env` values to participate in provider auth resolution, so providers such as Cloudflare can derive request-specific base URLs from explicit call options ([#6021](https://github.com/earendil-works/pi/issues/6021)).
+- Restored temporary legacy per-API stream aliases such as `streamSimpleOpenAICompletions` on the compat entrypoint ([#6016](https://github.com/earendil-works/pi/issues/6016), [#6017](https://github.com/earendil-works/pi/issues/6017)).
+- Restored runtime `detectCompat` fallback in `openai-completions` for models without explicit compat metadata ([#6020](https://github.com/earendil-works/pi/issues/6020)).
+
+## [0.80.1] - 2026-06-23
+
+### Fixed
+
+- Fixed a regression in Amazon Bedrock scoped `AWS_PROFILE` endpoint resolution for built-in inference profile endpoints.
+- Fixed Fireworks Anthropic-compatible requests to apply session-affinity and unsupported tool-field defaults for custom Fireworks models.
+- Fixed Together MiniMax M2.7 metadata to avoid unsupported Together reasoning toggles.
+
+## [0.80.0] - 2026-06-23
+
+### Breaking Changes
+
+- The root entrypoint (`@earendil-works/pi-ai`) is now core-only and side-effect free. The old global API moved to the temporary `@earendil-works/pi-ai/compat` entrypoint, a strict superset of the root: switching a file's import path is the only migration step. Moved symbols include `stream`/`complete`/`streamSimple`/`completeSimple`, `getModel`/`getModels`/`getProviders` (now deprecated aliases of `getBuiltinModel`/`getBuiltinModels`/`getBuiltinProviders` from `@earendil-works/pi-ai/providers/all`), `registerApiProvider`/`unregisterApiProviders`/`resetApiProviders`/`getApiProvider`, `getEnvApiKey`/`findEnvKeys`, `setBedrockProviderModule`, the per-API lazy stream wrappers (`anthropicMessagesApi`, ...), and the image-generation API.
+- Renamed the `Provider` type to `ProviderId`. `Provider` now names the runtime provider interface (id, name, auth, model listing, stream behavior).
+- API implementation modules moved from `src/providers/` to `@earendil-works/pi-ai/api/*`, renamed by API id (`anthropic` -> `api/anthropic-messages`, `google` -> `api/google-generative-ai`, `mistral` -> `api/mistral-conversations`, `amazon-bedrock` -> `api/bedrock-converse-stream`), each exporting exactly `stream` and `streamSimple`. The old per-impl export names (`streamAnthropic`, `streamSimpleAnthropic`, ...) and legacy raw API subpaths (`./anthropic`, `./google`, `./openai-completions`, ...) are gone; import raw API implementations through `@earendil-works/pi-ai/api/*`.
+- Removed the `@earendil-works/pi-ai/base` selective-provider entrypoint; use the root/core APIs with explicit `createModels()` collections and provider factories for isolated bundles.
+
+Migration guide:
+
+- Read `packages/ai/README.md` in full for the new `Models` API, provider factories, auth configuration, image generation, and custom provider examples.
+- To keep the old global API temporarily, change imports from `@earendil-works/pi-ai` to `@earendil-works/pi-ai/compat`. The compat entrypoint preserves `stream`/`complete`, generated catalog reads, API registry helpers, env API-key helpers, lazy API wrappers, and image globals, but it will be removed in a future release.
+- To migrate to the new runtime, create a `Models` collection and call methods on it:
+
+ ```ts
+ import { builtinModels } from "@earendil-works/pi-ai/providers/all";
+
+ const models = builtinModels();
+ const model = models.getModel("anthropic", "claude-haiku-4-5");
+ if (!model) throw new Error("model not found");
+
+ const message = await models.complete(model, {
+ messages: [{ role: "user", content: "Hello", timestamp: Date.now() }],
+ });
+ ```
+
+- For an isolated provider set, register provider factories explicitly:
+
+ ```ts
+ import { createModels } from "@earendil-works/pi-ai";
+ import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
+
+ const models = createModels();
+ models.setProvider(anthropicProvider());
+ ```
+
+- To call a raw API implementation directly, import from `@earendil-works/pi-ai/api/*` and pass a compatible model plus auth/options yourself. Raw API modules export `stream` and `streamSimple`; use `.result()` on the returned stream for `complete`/`completeSimple` behavior:
+
+ ```ts
+ import { streamSimple } from "@earendil-works/pi-ai/api/anthropic-messages";
+ import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all";
+
+ const model = getBuiltinModel("anthropic", "claude-haiku-4-5");
+ const stream = streamSimple(
+ model,
+ { messages: [{ role: "user", content: "Hello", timestamp: Date.now() }] },
+ { apiKey: process.env.ANTHROPIC_API_KEY },
+ );
+
+ const message = await stream.result();
+ ```
+
+ Custom raw models must set the matching `api` value (for example `"anthropic-messages"` for `api/anthropic-messages`) and any required provider compatibility metadata in `model.compat`.
+
+### Added
+
+- New `Models` runtime: `createModels()` builds an isolated provider collection with sync model reads (`getModels`/`getModel` return the last-known lists), an explicit async `refresh(provider?)` for dynamic providers, auth resolution (`getAuth`), and `stream`/`complete`/`streamSimple`/`completeSimple` that resolve auth through the owning provider. `createProvider()` builds providers from parts (single API implementation or a map dispatched on `model.api`; static `models` array plus an optional `refreshModels` fetcher with in-flight dedupe); `hasApi()` narrows dynamically listed models.
+- Provider auth substrate: `ProviderAuth` (`{ apiKey?, oauth? }`), one type-tagged credential per provider, `CredentialStore` (`read`/`modify`/`delete` with serialized writes; in-memory default), `envApiKeyAuth()`, `lazyOAuth()`, and injectable `AuthContext`. OAuth refresh runs under the store lock with double-checked expiry; a stored credential owns its provider (no silent env fallback).
+- One provider factory per built-in provider under `@earendil-works/pi-ai/providers/*` (e.g. `anthropicProvider()`, `openrouterProvider()`), plus `@earendil-works/pi-ai/providers/all` with `builtinProviders()`/`builtinModels()` and typed `getBuiltin*` catalog reads. Generated catalogs are split per provider, so importing one provider pulls one catalog; `sideEffects` metadata makes the package tree-shakeable.
+- OAuth flows (Anthropic, OpenAI Codex, GitHub Copilot) gained `OAuthAuth` adapters (`login`/`refresh`/`toAuth`) on unified `prompt()`/`notify()` login callbacks; Copilot's per-credential base URL is derived in `toAuth()`.
+- `fauxProvider()` returns a faux `Provider` for tests built on explicit `Models` collections.
+- Image generation mirrors the chat-side design: `createImagesModels()`/`ImagesProvider`/`createImagesProvider()` with sync model reads, explicit `refresh()`, provider-resolved auth, and never-rejecting `generateImages()`; `openrouterImagesProvider()` factory plus `builtinImagesProviders()`/`builtinImagesModels()` in `providers/all`. The `ImagesProvider` id type alias is renamed to `ImagesProviderId`; the old global image API stays on `/compat`.
+- Provider auth results can carry provider-scoped environment values that `Models` and `ImagesModels` merge into API implementation options.
+
+### Fixed
+
+- Fixed OpenAI Responses streams to fail when they end before a terminal response event and to treat `response.incomplete` as a length stop ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)).
+- Fixed Amazon Bedrock endpoint resolution to honor scoped `AWS_PROFILE` values.
+- Fixed Cloudflare providers to require account/gateway configuration and route built-in `/compat` requests through provider auth.
+- Fixed `/compat` API-key injection to honor request-scoped `env` values.
+- Fixed OpenAI Codex Responses WebSocket sessions to reconnect once when OpenAI's connection limit is reached before output starts ([#5973](https://github.com/earendil-works/pi/issues/5973)).
+- Fixed OpenCode Go GLM-5.2 metadata to expose `xhigh` reasoning and send `reasoning_effort: "max"` ([#5967](https://github.com/earendil-works/pi/issues/5967)).
+
+## [0.79.10] - 2026-06-22
+
+### Fixed
+
+- Fixed OpenAI-compatible streaming to preserve encrypted `reasoning_details` that arrive before matching tool call deltas ([#5114](https://github.com/earendil-works/pi/issues/5114)).
+
+## [0.79.9] - 2026-06-20
+
+### Added
+
+- Added configurable `chat-template` thinking support for OpenAI-compatible providers that use `chat_template_kwargs`, such as DeepSeek models behind vLLM ([#5673](https://github.com/earendil-works/pi/issues/5673)).
+
+### Fixed
+
+- Fixed Fireworks GLM-5.2 metadata to use the OpenAI-compatible Chat Completions endpoint with `reasoning_effort` support ([#5923](https://github.com/earendil-works/pi/issues/5923)).
+- Fixed OpenRouter GLM-5.2 metadata to expose `xhigh` reasoning and send OpenRouter's native `xhigh` effort ([#5770](https://github.com/earendil-works/pi/issues/5770)).
+- Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)).
+
+## [0.79.8] - 2026-06-19
+
+### Added
+
+- Added `@earendil-works/pi-ai/base` and direct provider registration exports for bundlers that want selective provider transports without root built-in registration ([#5348](https://github.com/earendil-works/pi/pull/5348) by [@FredKSchott](https://github.com/FredKSchott)).
+- Added prompt caching for Mistral requests using the pi session ID as `prompt_cache_key`, including cached-token usage and cost accounting ([#5854](https://github.com/earendil-works/pi/issues/5854)).
+- Added the OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)).
+
+## [0.79.7] - 2026-06-18
+
### Added
- Added GLM-5.2 model to the OpenCode Go subscription model catalog ([#5860](https://github.com/earendil-works/pi/issues/5860)).
diff --git a/packages/ai/README.md b/packages/ai/README.md
index a7b8028e..239f559a 100644
--- a/packages/ai/README.md
+++ b/packages/ai/README.md
@@ -1,6 +1,6 @@
# @earendil-works/pi-ai
-Unified LLM API with automatic model discovery, provider configuration, token and cost tracking, and simple context persistence and hand-off to other models mid-session.
+Unified LLM API with provider collections, automatic auth resolution, token and cost tracking, and simple context persistence and hand-off to other models mid-session.
**Note**: This library only includes models that support tool calling (function calling), as this is essential for agentic workflows.
@@ -9,6 +9,16 @@ Unified LLM API with automatic model discovery, provider configuration, token an
- [Supported Providers](#supported-providers)
- [Installation](#installation)
- [Quick Start](#quick-start)
+- [Providers and Models](#providers-and-models)
+ - [Provider Factories](#provider-factories)
+ - [All Built-in Providers](#all-built-in-providers)
+ - [Querying Models](#querying-models)
+ - [Static Catalog Reads](#static-catalog-reads)
+ - [Dynamic Providers](#dynamic-providers)
+- [Auth](#auth)
+ - [How Auth Resolves](#how-auth-resolves)
+ - [Credential Store](#credential-store)
+ - [Environment Variables](#environment-variables)
- [Tools](#tools)
- [Defining Tools](#defining-tools)
- [Handling Tool Calls](#handling-tool-calls)
@@ -17,8 +27,6 @@ Unified LLM API with automatic model discovery, provider configuration, token an
- [Complete Event Reference](#complete-event-reference)
- [Image Input](#image-input)
- [Image Generation](#image-generation)
- - [Basic Image Generation](#basic-image-generation)
- - [Notes and Limitations](#notes-and-limitations)
- [Thinking/Reasoning](#thinkingreasoning)
- [Unified Interface](#unified-interface-streamsimplecompletesimple)
- [Provider-Specific Options](#provider-specific-options-streamcomplete)
@@ -27,26 +35,22 @@ Unified LLM API with automatic model discovery, provider configuration, token an
- [Error Handling](#error-handling)
- [Aborting Requests](#aborting-requests)
- [Continuing After Abort](#continuing-after-abort)
-- [APIs, Models, and Providers](#apis-models-and-providers)
- - [Providers and Models](#providers-and-models)
- - [Querying Providers and Models](#querying-providers-and-models)
- - [Custom Models](#custom-models)
+ - [Debugging Provider Payloads](#debugging-provider-payloads)
+- [Custom Providers](#custom-providers)
+ - [createProvider()](#createprovider)
+ - [Calling API Implementations Directly](#calling-api-implementations-directly)
- [OpenAI Compatibility Settings](#openai-compatibility-settings)
- - [Type Safety](#type-safety)
+- [Faux Provider for Tests](#faux-provider-for-tests)
- [Cross-Provider Handoffs](#cross-provider-handoffs)
- [Context Serialization](#context-serialization)
- [Browser Usage](#browser-usage)
- - [Browser Compatibility Notes](#browser-compatibility-notes)
- - [Environment Variables](#environment-variables-nodejs-only)
- - [Provider-Scoped Environment Overrides](#provider-scoped-environment-overrides)
- - [Checking Environment Variables](#checking-environment-variables)
+- [Bundling and Tree Shaking](#bundling-and-tree-shaking)
- [OAuth Providers](#oauth-providers)
- [Vertex AI](#vertex-ai)
- [CLI Login](#cli-login)
- [Programmatic OAuth](#programmatic-oauth)
- - [Login Flow Example](#login-flow-example)
- - [Using OAuth Tokens](#using-oauth-tokens)
- - [Provider Notes](#provider-notes)
+- [Migrating from the Old Global API](#migrating-from-the-old-global-api)
+- [Development](#development)
- [License](#license)
## Supported Providers
@@ -68,16 +72,18 @@ Unified LLM API with automatic model discovery, provider configuration, token an
- **xAI**
- **OpenRouter**
- **Vercel AI Gateway**
-- **ZAI** (with separate Coding Plan China provider)
-- **MiniMax**
+- **ZAI Coding Plan (Global)** (with separate China provider)
+- **MiniMax** (with separate China provider)
- **Together AI**
+- **Hugging Face**
+- **Moonshot AI** (with separate China provider)
- **GitHub Copilot** (requires OAuth, see below)
- **Amazon Bedrock**
- **OpenCode Zen**
- **OpenCode Go**
-- **Fireworks** (uses Anthropic-compatible API)
-- **Kimi For Coding** (Moonshot AI, uses Anthropic-compatible API)
-- **Xiaomi MiMo** (uses Anthropic-compatible API; defaults to API billing endpoint, with separate Token Plan providers for `cn`/`ams`/`sgp` regions)
+- **Fireworks** (uses OpenAI- and Anthropic-compatible APIs)
+- **Kimi For Coding** (Moonshot AI subscription endpoint, uses Anthropic-compatible API)
+- **Xiaomi MiMo** (defaults to API billing endpoint, with separate Token Plan providers for `cn`/`ams`/`sgp` regions)
- **Any OpenAI-compatible API**: Ollama, vLLM, LM Studio, etc.
## Installation
@@ -90,11 +96,17 @@ TypeBox exports are re-exported from `@earendil-works/pi-ai`: `Type`, `Static`,
## Quick Start
-```typescript
-import { Type, getModel, stream, complete, Context, Tool, StringEnum } from '@earendil-works/pi-ai';
+You build a `Models` collection of providers and stream through it. The quickest start registers every built-in provider; apps that care about bundle size register individual providers instead (see [Provider Factories](#provider-factories) and [Bundling and Tree Shaking](#bundling-and-tree-shaking)).
-// Fully typed with auto-complete support for both providers and models
-const model = getModel('openai', 'gpt-4o-mini');
+```typescript
+import { Type, type Context, type Tool } from '@earendil-works/pi-ai';
+import { builtinModels } from '@earendil-works/pi-ai/providers/all';
+
+// A Models collection with every built-in provider registered
+const models = builtinModels();
+
+// Sync lookup against the collection
+const model = models.getModel('openai', 'gpt-4o-mini')!;
// Define tools with TypeBox schemas for type safety and validation
const tools: Tool[] = [{
@@ -108,12 +120,13 @@ const tools: Tool[] = [{
// Build a conversation context (easily serializable and transferable between models)
const context: Context = {
systemPrompt: 'You are a helpful assistant.',
- messages: [{ role: 'user', content: 'What time is it?' }],
+ messages: [{ role: 'user', content: 'What time is it?', timestamp: Date.now() }],
tools
};
-// Option 1: Streaming with all event types
-const s = stream(model, context);
+// Option 1: Streaming with all event types.
+// Auth resolves through the provider (OPENAI_API_KEY from the environment here).
+const s = models.stream(model, context);
for await (const event of s) {
switch (event.type) {
@@ -156,7 +169,7 @@ for await (const event of s) {
console.log(`\nFinished: ${event.reason}`);
break;
case 'error':
- console.error(`Error: ${event.error}`);
+ console.error(`Error: ${event.error.errorMessage}`);
break;
}
}
@@ -168,7 +181,6 @@ context.messages.push(finalMessage);
// Handle tool calls if any
const toolCalls = finalMessage.content.filter(b => b.type === 'toolCall');
for (const call of toolCalls) {
- // Execute the tool
const result = call.name === 'get_time'
? new Date().toLocaleString('en-US', {
timeZone: call.arguments.timezone || 'UTC',
@@ -190,7 +202,7 @@ for (const call of toolCalls) {
// Continue if there were tool calls
if (toolCalls.length > 0) {
- const continuation = await complete(model, context);
+ const continuation = await models.complete(model, context);
context.messages.push(continuation);
console.log('After tool execution:', continuation.content);
}
@@ -199,7 +211,7 @@ console.log(`Total tokens: ${finalMessage.usage.input} in, ${finalMessage.usage.
console.log(`Cost: $${finalMessage.usage.cost.total.toFixed(4)}`);
// Option 2: Get complete response without streaming
-const response = await complete(model, context);
+const response = await models.complete(model, context);
for (const block of response.content) {
if (block.type === 'text') {
@@ -210,6 +222,198 @@ for (const block of response.content) {
}
```
+Snippets in the rest of this README assume a `models` collection set up like this (with the relevant providers registered).
+
+## Providers and Models
+
+A **provider** is the runtime unit: it owns its model catalog, its auth (API key resolution, OAuth flows), and its stream behavior. A `Models` collection holds providers and routes every request to the provider that owns the model.
+
+Providers internally share **API implementations** (the wire protocols): Anthropic models use `anthropic-messages`, OpenAI uses `openai-responses`, while xAI, Groq, Cerebras, OpenRouter, and most others share `openai-completions`. Mixed-API providers (GitHub Copilot, OpenCode Zen) dispatch per model.
+
+### Provider Factories
+
+For apps that only need specific providers, there is one factory per built-in provider, each a subpath import that pulls only that provider's catalog:
+
+```typescript
+import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic';
+import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';
+import { openrouterProvider } from '@earendil-works/pi-ai/providers/openrouter';
+import { amazonBedrockProvider } from '@earendil-works/pi-ai/providers/amazon-bedrock';
+// ...one module per provider in the Supported Providers list
+
+const models = createModels();
+models.setProvider(anthropicProvider());
+models.setProvider(openrouterProvider());
+```
+
+Provider factories import their model catalog and a lazy API wrapper. They do not import other providers. With bundler code splitting, SDK implementations (`@anthropic-ai/sdk`, `openai`, `@google/genai`, etc.) stay in lazy chunks loaded on the first request to a model of that API.
+
+### All Built-in Providers
+
+For apps that want everything (as in Quick Start):
+
+```typescript
+import { builtinModels } from '@earendil-works/pi-ai/providers/all';
+
+const models = builtinModels(); // a Models collection with every built-in provider registered
+```
+
+This imports all catalogs and every built-in provider factory. It is the heavy, explicit entrypoint. `builtinModels()` accepts the same options as `createModels()` (`credentials`, `authContext`); `builtinProviders()` returns the provider array if you want to register them on your own collection.
+
+### Querying Models
+
+Reads are synchronous and return the last-known lists:
+
+```typescript
+const providers = models.getProviders(); // registered Provider objects
+const provider = models.getProvider('anthropic'); // one provider
+
+const all = models.getModels(); // every model across providers
+const anthropicModels = models.getModels('anthropic');
+const model = models.getModel('anthropic', 'claude-sonnet-4-5');
+
+for (const m of anthropicModels) {
+ console.log(`${m.id}: ${m.name}`);
+ console.log(` API: ${m.api}`);
+ console.log(` Context: ${m.contextWindow} tokens`);
+ console.log(` Vision: ${m.input.includes('image')}`);
+ console.log(` Reasoning: ${m.reasoning}`);
+}
+```
+
+Dynamically listed models are typed `Model`. Narrow with the `hasApi()` guard when you need API-specific option typing:
+
+```typescript
+import { hasApi } from '@earendil-works/pi-ai';
+
+const m = models.getModel('anthropic', 'claude-sonnet-4-5');
+if (m && hasApi(m, 'anthropic-messages')) {
+ // m: Model<'anthropic-messages'> — stream options fully typed
+ models.stream(m, context, { thinkingEnabled: true, thinkingBudgetTokens: 2048 });
+}
+```
+
+### Static Catalog Reads
+
+For tooling that wants the generated built-in catalog with full literal typing (provider and model IDs auto-complete), independent of any collection:
+
+```typescript
+import { getBuiltinModel, getBuiltinModels, getBuiltinProviders } from '@earendil-works/pi-ai/providers/all';
+
+const model = getBuiltinModel('openai', 'gpt-4o-mini'); // typed Model<'openai-responses'>
+const providers = getBuiltinProviders();
+const anthropic = getBuiltinModels('anthropic');
+```
+
+### Dynamic Providers
+
+Providers may have dynamic model lists (a llama.cpp server, a live OpenRouter listing). Reads stay sync; fetching is an explicit async verb:
+
+```typescript
+// getModels() returns the last-known list (empty before the first refresh)
+await models.refresh('llamacpp'); // fetch one provider's list; rejects on failure
+await models.refresh(); // refresh all providers concurrently, best-effort
+const fresh = models.getModel('llamacpp', 'qwen3-30b');
+```
+
+Static built-in providers are no-ops for `refresh()`. See [createProvider()](#createprovider) for building a dynamic provider.
+
+## Auth
+
+Every provider owns its auth: how API keys resolve (stored credentials, environment variables, ambient sources like AWS profiles or gcloud ADC) and, where supported, OAuth login/refresh flows.
+
+### How Auth Resolves
+
+When you call `models.stream()`, the collection resolves auth through the owning provider and merges it into the request. Explicit per-request values always win:
+
+```typescript
+// Resolved through the provider (env var, stored credential, OAuth token):
+await models.complete(model, context);
+
+// Explicit key wins over anything the provider would resolve:
+await models.complete(model, context, { apiKey: 'sk-explicit' });
+```
+
+You can inspect resolution without making a request — useful for status UIs:
+
+```typescript
+const auth = await models.getAuth(model);
+if (auth) {
+ console.log(`configured via ${auth.source}`); // e.g. "ANTHROPIC_API_KEY", "OAuth", "stored credential"
+} else {
+ console.log('not configured');
+}
+```
+
+`getAuth()` resolves `undefined` for unconfigured providers and rejects with `ModelsError` when something is actually broken (`"oauth"`: token refresh failed, credential preserved for re-login; `"auth"`: key resolution or credential store failure). Request paths surface the same failures as stream errors.
+
+### Credential Store
+
+Stored credentials (API keys entered interactively, OAuth tokens) live in a `CredentialStore` — one type-tagged credential per provider. pi-ai ships an in-memory default; apps inject persistent storage:
+
+```typescript
+import { createModels, type CredentialStore } from '@earendil-works/pi-ai';
+
+const models = createModels({ credentials: myFileBackedStore });
+// builtinModels() takes the same options:
+// const models = builtinModels({ credentials: myFileBackedStore });
+```
+
+The contract is small: `read(providerId)`, `modify(providerId, fn)` (the only write path — a serialized read-modify-write), and `delete(providerId)`. OAuth token refresh runs inside `modify`, so concurrent requests and processes cannot double-refresh a rotated token. A stored credential *owns* its provider: environment variables are only consulted when nothing is stored, and a failed refresh never silently falls back to an env key.
+
+API-key credentials use the same discriminator as pi's `auth.json` and can carry provider-scoped env/config values:
+
+```typescript
+const credential = {
+ type: 'api_key',
+ key: '...',
+ env: {
+ CLOUDFLARE_ACCOUNT_ID: 'account-id',
+ CLOUDFLARE_GATEWAY_ID: 'gateway-id'
+ }
+} as const;
+```
+
+### Environment Variables
+
+Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` explicitly):
+
+| Provider | Environment Variable(s) |
+|----------|------------------------|
+| OpenAI | `OPENAI_API_KEY` |
+| Ant Ling | `ANT_LING_API_KEY` |
+| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` (e.g. `https://{resource}.ai.azure.com`) or `AZURE_OPENAI_RESOURCE_NAME`. Supports `*.openai.azure.com`, `*.cognitiveservices.azure.com` and `*.ai.azure.com`; root endpoints auto-normalize to `/openai/v1`. Optional: `AZURE_OPENAI_API_VERSION` (default `v1`), `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. |
+| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` |
+| DeepSeek | `DEEPSEEK_API_KEY` |
+| NVIDIA NIM | `NVIDIA_API_KEY` |
+| Google | `GEMINI_API_KEY` |
+| Vertex AI | `GOOGLE_CLOUD_API_KEY` or `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |
+| Mistral | `MISTRAL_API_KEY` |
+| Groq | `GROQ_API_KEY` |
+| Cerebras | `CEREBRAS_API_KEY` |
+| Cloudflare AI Gateway | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_GATEWAY_ID` |
+| Cloudflare Workers AI | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` |
+| xAI | `XAI_API_KEY` |
+| Fireworks | `FIREWORKS_API_KEY` |
+| Together AI | `TOGETHER_API_KEY` |
+| OpenRouter | `OPENROUTER_API_KEY` |
+| Vercel AI Gateway | `AI_GATEWAY_API_KEY` |
+| ZAI Coding Plan (Global) | `ZAI_API_KEY` |
+| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` |
+| MiniMax (Global) | `MINIMAX_API_KEY` |
+| MiniMax (China) | `MINIMAX_CN_API_KEY` |
+| Moonshot AI / Moonshot AI (China) | `MOONSHOT_API_KEY` |
+| Hugging Face | `HF_TOKEN` |
+| OpenCode Zen / OpenCode Go | `OPENCODE_API_KEY` |
+| Kimi For Coding | `KIMI_API_KEY` |
+| Xiaomi MiMo (API billing) | `XIAOMI_API_KEY` |
+| Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` |
+| Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` |
+| Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` |
+| GitHub Copilot | `COPILOT_GITHUB_TOKEN` |
+
+Amazon Bedrock resolves ambient AWS credentials (`AWS_PROFILE`, access key pairs, `AWS_BEARER_TOKEN_BEDROCK`, ECS task roles, web identity tokens). Vertex AI resolves either an explicit key or gcloud Application Default Credentials plus project/location.
+
## Tools
Tools enable LLMs to interact with external systems. This library uses TypeBox schemas for type-safe tool definitions with automatic validation using TypeBox's built-in validator and value conversion utilities. TypeBox schemas can be serialized and deserialized as plain JSON, making them ideal for distributed systems.
@@ -217,7 +421,7 @@ Tools enable LLMs to interact with external systems. This library uses TypeBox s
### Defining Tools
```typescript
-import { Type, Tool, StringEnum } from '@earendil-works/pi-ai';
+import { Type, type Tool, StringEnum } from '@earendil-works/pi-ai';
// Define tool parameters with TypeBox
const weatherTool: Tool = {
@@ -252,11 +456,11 @@ Tool results use content blocks and can include both text and images:
import { readFileSync } from 'fs';
const context: Context = {
- messages: [{ role: 'user', content: 'What is the weather in London?' }],
+ messages: [{ role: 'user', content: 'What is the weather in London?', timestamp: Date.now() }],
tools: [weatherTool]
};
-const response = await complete(model, context);
+const response = await models.complete(model, context);
// Check for tool calls in the response
for (const block of response.content) {
@@ -297,7 +501,7 @@ context.messages.push({
During streaming, tool call arguments are progressively parsed as they arrive. This enables real-time UI updates before the complete arguments are available:
```typescript
-const s = stream(model, context);
+const s = models.stream(model, context);
for await (const event of s) {
if (event.type === 'toolcall_delta') {
@@ -338,15 +542,13 @@ for await (const event of s) {
### Validating Tool Arguments
-When using `agentLoop`, tool arguments are automatically validated against your TypeBox schemas before execution. If validation fails, the error is returned to the model as a tool result, allowing it to retry.
-
-When implementing your own tool execution loop with `stream()` or `complete()`, use `validateToolCall` to validate arguments before passing them to your tools:
+When implementing your own tool execution loop, use `validateToolCall` to validate arguments before passing them to your tools:
```typescript
-import { stream, validateToolCall, Tool } from '@earendil-works/pi-ai';
+import { validateToolCall, type Tool } from '@earendil-works/pi-ai';
const tools: Tool[] = [weatherTool, calculatorTool];
-const s = stream(model, { messages, tools });
+const s = models.stream(model, { messages, tools });
for await (const event of s) {
if (event.type === 'toolcall_end') {
@@ -399,9 +601,8 @@ Models with vision capabilities can process images. You can check if a model sup
```typescript
import { readFileSync } from 'fs';
-import { getModel, complete } from '@earendil-works/pi-ai';
-const model = getModel('openai', 'gpt-4o-mini');
+const model = models.getModel('openai', 'gpt-4o-mini')!;
// Check if model supports images
if (model.input.includes('image')) {
@@ -411,13 +612,14 @@ if (model.input.includes('image')) {
const imageBuffer = readFileSync('image.png');
const base64Image = imageBuffer.toString('base64');
-const response = await complete(model, {
+const response = await models.complete(model, {
messages: [{
role: 'user',
content: [
{ type: 'text', text: 'What is in this image?' },
{ type: 'image', data: base64Image, mimeType: 'image/png' }
- ]
+ ],
+ timestamp: Date.now()
}]
});
@@ -431,21 +633,21 @@ for (const block of response.content) {
## Image Generation
-Image generation uses a separate API surface from text/chat generation. Use `getImageModel()` / `getImageModels()` / `getImageProviders()` to discover image-generation models, and `generateImages()` to get the final result.
-
-Do not use `stream()` or `complete()` for image generation. Image generation is a one-shot API: `generateImages()` waits for the provider response and returns the final `AssistantImages` result.
+Image generation uses a separate API surface from text/chat generation, mirroring the chat-side design: an `ImagesModels` collection holds `ImagesProvider`s, reads are sync, and auth resolves through the owning provider. Image generation is a one-shot API: `generateImages()` waits for the provider response and returns the final `AssistantImages` result — do not use the chat/stream APIs for it.
### Basic Image Generation
```typescript
-import { getImageModel, generateImages } from '@mariozechner/pi-ai';
+import { builtinImagesModels } from '@earendil-works/pi-ai/providers/all';
-const model = getImageModel('openrouter', 'google/gemini-2.5-flash-image');
+// Every built-in image-generation provider; accepts the same options as createModels()
+const imagesModels = builtinImagesModels();
-const result = await generateImages(model, {
+const model = imagesModels.getModel('openrouter', 'google/gemini-2.5-flash-image')!;
+
+// Auth resolves through the provider (OPENROUTER_API_KEY here); explicit apiKey wins
+const result = await imagesModels.generateImages(model, {
input: [{ type: 'text', text: 'Generate a red circle on a plain white background.' }]
-}, {
- apiKey: process.env.OPENROUTER_API_KEY
});
for (const block of result.output) {
@@ -458,19 +660,32 @@ for (const block of result.output) {
}
```
+Like the chat side, you can build the collection from parts: `createImagesModels({ credentials?, authContext? })`, the `openrouterImagesProvider()` factory from `@earendil-works/pi-ai/providers/openrouter-images`, and `createImagesProvider({ id, auth, models, refreshModels?, api })` for custom image providers (with `imagesModels.refresh(provider?)` for dynamic lists). Failures never reject — they return an `AssistantImages` with `stopReason: "error"`. The collection's `getAuth(model)` works exactly like the chat-side one.
+
+The old global API (`getImageModel()` / `getImageModels()` / `getImageProviders()` / `generateImages()`) remains available on the [compat entrypoint](#migrating-from-the-old-global-api):
+
+```typescript
+import { getImageModel, generateImages } from '@earendil-works/pi-ai/compat';
+
+const model = getImageModel('openrouter', 'google/gemini-2.5-flash-image');
+const result = await generateImages(model, {
+ input: [{ type: 'text', text: 'Generate a red circle on a plain white background.' }]
+}, {
+ apiKey: process.env.OPENROUTER_API_KEY
+});
+```
+
Some models also support image input:
```typescript
import { readFileSync } from 'fs';
const imageBuffer = readFileSync('input.png');
-const result = await generateImages(model, {
+const result = await imagesModels.generateImages(model, {
input: [
{ type: 'text', text: 'Create a variation of this image with a blue background.' },
{ type: 'image', data: imageBuffer.toString('base64'), mimeType: 'image/png' }
]
-}, {
- apiKey: process.env.OPENROUTER_API_KEY
});
```
@@ -483,14 +698,14 @@ console.log(model.output); // ['image'] or ['image', 'text']
### Notes and Limitations
-- Use `getImageModel(...)`, not `getModel(...)`.
-- Use `generateImages()`, not `stream()` / `complete()`.
+- Image models live in `ImagesModels` collections, chat models in `Models` collections; the two are separate surfaces.
+- Use `generateImages()`, not the chat/stream APIs.
- Image-generation models do not participate in tool calling.
- Outputs are returned in `AssistantImages.output` and can include both base64-encoded `ImageContent` blocks and `TextContent` blocks.
- Some models return only images, others return images plus text. Check `model.output`.
- Some models accept image input, others are text-to-image only. Check `model.input`.
- Like the streaming APIs, image generation supports options such as `apiKey`, `signal`, `headers`, `onPayload`, and `onResponse`, and results may include `stopReason`, `responseId`, and `usage`.
-- If you want a model to analyze images in a conversation or call tools, use the regular `stream()` / `complete()` APIs with a model that supports image input.
+- If you want a model to analyze images in a conversation or call tools, use the regular chat APIs with a model that supports image input.
- At the moment, image generation is available through only one provider, OpenRouter.
## Thinking/Reasoning
@@ -500,16 +715,11 @@ Many models support thinking/reasoning capabilities where they can show their in
### Unified Interface (streamSimple/completeSimple)
```typescript
-import { getModel, streamSimple, completeSimple } from '@earendil-works/pi-ai';
-
// Many models across providers support thinking/reasoning
-const model = getModel('anthropic', 'claude-sonnet-4-20250514');
-// or getModel('openai', 'gpt-5-mini');
-// or getModel('google', 'gemini-2.5-flash');
-// or getModel('xai', 'grok-code-fast-1');
-// or getModel('groq', 'openai/gpt-oss-20b');
-// or getModel('cerebras', 'gpt-oss-120b');
-// or getModel('openrouter', 'z-ai/glm-4.5v');
+const model = models.getModel('anthropic', 'claude-sonnet-4-5')!;
+// or models.getModel('openai', 'gpt-5-mini');
+// or models.getModel('google', 'gemini-2.5-flash');
+// or models.getModel('xai', 'grok-code-fast-1');
// Check if model supports reasoning
if (model.reasoning) {
@@ -517,8 +727,8 @@ if (model.reasoning) {
}
// Use the simplified reasoning option
-const response = await completeSimple(model, {
- messages: [{ role: 'user', content: 'Solve: 2x + 5 = 13' }]
+const response = await models.completeSimple(model, {
+ messages: [{ role: 'user', content: 'Solve: 2x + 5 = 13', timestamp: Date.now() }]
}, {
reasoning: 'medium' // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'
});
@@ -535,33 +745,39 @@ for (const block of response.content) {
### Provider-Specific Options (stream/complete)
-For fine-grained control, use the provider-specific options:
+`models.stream()`/`complete()` accept the owning API's full option set. Use `hasApi()` to narrow a dynamically looked-up model to its API for full option typing:
```typescript
-import { getModel, complete } from '@earendil-works/pi-ai';
+import { hasApi } from '@earendil-works/pi-ai';
// OpenAI Reasoning (o1, o3, gpt-5)
-const openaiModel = getModel('openai', 'gpt-5-mini');
-await complete(openaiModel, context, {
- reasoningEffort: 'medium',
- reasoningSummary: 'detailed' // OpenAI Responses API only
-});
+const openaiModel = models.getModel('openai', 'gpt-5-mini')!;
+if (hasApi(openaiModel, 'openai-responses')) {
+ await models.complete(openaiModel, context, {
+ reasoningEffort: 'medium',
+ reasoningSummary: 'detailed' // OpenAI Responses API only
+ });
+}
-// Anthropic Thinking (Claude Sonnet 4)
-const anthropicModel = getModel('anthropic', 'claude-sonnet-4-20250514');
-await complete(anthropicModel, context, {
- thinkingEnabled: true,
- thinkingBudgetTokens: 8192 // Optional token limit
-});
+// Anthropic Thinking
+const anthropicModel = models.getModel('anthropic', 'claude-sonnet-4-5')!;
+if (hasApi(anthropicModel, 'anthropic-messages')) {
+ await models.complete(anthropicModel, context, {
+ thinkingEnabled: true,
+ thinkingBudgetTokens: 8192 // Optional token limit
+ });
+}
// Google Gemini Thinking
-const googleModel = getModel('google', 'gemini-2.5-flash');
-await complete(googleModel, context, {
- thinking: {
- enabled: true,
- budgetTokens: 8192 // -1 for dynamic, 0 to disable
- }
-});
+const googleModel = models.getModel('google', 'gemini-2.5-flash')!;
+if (hasApi(googleModel, 'google-generative-ai')) {
+ await models.complete(googleModel, context, {
+ thinking: {
+ enabled: true,
+ budgetTokens: 8192 // -1 for dynamic, 0 to disable
+ }
+ });
+}
```
### Streaming Thinking Content
@@ -569,7 +785,7 @@ await complete(googleModel, context, {
When streaming, thinking content is delivered through specific events:
```typescript
-const s = streamSimple(model, context, { reasoning: 'high' });
+const s = models.streamSimple(model, context, { reasoning: 'high' });
for await (const event of s) {
switch (event.type) {
@@ -600,11 +816,11 @@ Every `AssistantMessage` includes a `stopReason` field that indicates how the ge
## Error Handling
-When a request ends with an error (including aborts and tool call validation errors), the streaming API emits an error event:
+Request failures never throw out of the stream functions: when a request ends with an error (including aborts and tool call validation errors), the streaming API emits an error event and the final message carries the details:
```typescript
// In streaming
-for await (const event of stream) {
+for await (const event of s) {
if (event.type === 'error') {
// event.reason is either "error" or "aborted"
// event.error is the AssistantMessage with partial content
@@ -614,7 +830,7 @@ for await (const event of stream) {
}
// The final message will have the error details
-const message = await stream.result();
+const message = await s.result();
if (message.stopReason === 'error' || message.stopReason === 'aborted') {
console.error('Request failed:', message.errorMessage);
// message.content contains any partial content received before the error
@@ -622,21 +838,20 @@ if (message.stopReason === 'error' || message.stopReason === 'aborted') {
}
```
+Auth failures (no key configured, OAuth refresh failed, unknown provider) surface the same way: as a stream error with `stopReason: "error"`.
+
### Aborting Requests
The abort signal allows you to cancel in-progress requests. Aborted requests have `stopReason === 'aborted'`:
```typescript
-import { getModel, stream } from '@earendil-works/pi-ai';
-
-const model = getModel('openai', 'gpt-4o-mini');
const controller = new AbortController();
// Abort after 2 seconds
setTimeout(() => controller.abort(), 2000);
-const s = stream(model, {
- messages: [{ role: 'user', content: 'Write a long story' }]
+const s = models.stream(model, {
+ messages: [{ role: 'user', content: 'Write a long story', timestamp: Date.now() }]
}, {
signal: controller.signal
});
@@ -666,7 +881,7 @@ Aborted messages can be added to the conversation context and continued in subse
```typescript
const context = {
messages: [
- { role: 'user', content: 'Explain quantum computing in detail' }
+ { role: 'user', content: 'Explain quantum computing in detail', timestamp: Date.now() }
]
};
@@ -674,14 +889,14 @@ const context = {
const controller1 = new AbortController();
setTimeout(() => controller1.abort(), 2000);
-const partial = await complete(model, context, { signal: controller1.signal });
+const partial = await models.complete(model, context, { signal: controller1.signal });
// Add the partial response to context
context.messages.push(partial);
-context.messages.push({ role: 'user', content: 'Please continue' });
+context.messages.push({ role: 'user', content: 'Please continue', timestamp: Date.now() });
// Continue the conversation
-const continuation = await complete(model, context);
+const continuation = await models.complete(model, context);
```
### Debugging Provider Payloads
@@ -689,7 +904,7 @@ const continuation = await complete(model, context);
Use the `onPayload` callback to inspect the request payload sent to the provider. This is useful for debugging request formatting issues or provider validation errors.
```typescript
-const response = await complete(model, context, {
+const response = await models.complete(model, context, {
onPayload: (payload) => {
console.log('Provider payload:', JSON.stringify(payload, null, 2));
}
@@ -698,147 +913,16 @@ const response = await complete(model, context, {
The callback is supported by `stream`, `complete`, `streamSimple`, and `completeSimple`.
-## APIs, Models, and Providers
+## Custom Providers
-The library uses a registry of API implementations. Built-in APIs include:
+### createProvider()
-- **`anthropic-messages`**: Anthropic Messages API (`streamAnthropic`, `AnthropicOptions`)
-- **`google-generative-ai`**: Google Generative AI API (`streamGoogle`, `GoogleOptions`)
-- **`google-vertex`**: Google Vertex AI API (`streamGoogleVertex`, `GoogleVertexOptions`)
-- **`mistral-conversations`**: Mistral Conversations API (`streamMistral`, `MistralOptions`)
-- **`openai-completions`**: OpenAI Chat Completions API (`streamOpenAICompletions`, `OpenAICompletionsOptions`)
-- **`openai-responses`**: OpenAI Responses API (`streamOpenAIResponses`, `OpenAIResponsesOptions`)
-- **`openai-codex-responses`**: OpenAI Codex Responses API (`streamOpenAICodexResponses`, `OpenAICodexResponsesOptions`)
-- **`azure-openai-responses`**: Azure OpenAI Responses API (`streamAzureOpenAIResponses`, `AzureOpenAIResponsesOptions`)
-- **`bedrock-converse-stream`**: Amazon Bedrock Converse API (`streamBedrock`, `BedrockOptions`)
-
-### Faux provider for tests
-
-`registerFauxProvider()` registers a temporary in-memory provider for tests and demos. It is opt-in and not part of the built-in provider set.
+`createProvider()` builds a provider from parts: identity, auth, a model list, and an API implementation. Use it for local inference servers, proxies, or any OpenAI/Anthropic-compatible endpoint:
```typescript
-import {
- complete,
- fauxAssistantMessage,
- fauxText,
- fauxThinking,
- fauxToolCall,
- registerFauxProvider,
- stream,
-} from '@earendil-works/pi-ai';
+import { createModels, createProvider, envApiKeyAuth, type Model } from '@earendil-works/pi-ai';
+import { openAICompletionsApi } from '@earendil-works/pi-ai/api/openai-completions.lazy';
-const registration = registerFauxProvider({
- tokensPerSecond: 50 // optional
-});
-
-const model = registration.getModel();
-const context = {
- messages: [{ role: 'user', content: 'Summarize package.json and then call echo', timestamp: Date.now() }]
-};
-
-registration.setResponses([
- fauxAssistantMessage([
- fauxThinking('Need to inspect package metadata first.'),
- fauxToolCall('echo', { text: 'package.json' })
- ], { stopReason: 'toolUse' })
-]);
-
-const first = await complete(model, context, {
- sessionId: 'session-1',
- cacheRetention: 'short'
-});
-context.messages.push(first);
-
-context.messages.push({
- role: 'toolResult',
- toolCallId: first.content.find((block) => block.type === 'toolCall')!.id,
- toolName: 'echo',
- content: [{ type: 'text', text: 'package.json contents here' }],
- isError: false,
- timestamp: Date.now()
-});
-
-registration.setResponses([
- fauxAssistantMessage([
- fauxThinking('Now I can summarize the tool output.'),
- fauxText('Here is the summary.')
- ])
-]);
-
-const s = stream(model, context);
-for await (const event of s) {
- console.log(event.type);
-}
-
-// Optional: register multiple faux models for model-switching tests
-const multiModel = registerFauxProvider({
- models: [
- { id: 'faux-fast', reasoning: false },
- { id: 'faux-thinker', reasoning: true }
- ]
-});
-const thinker = multiModel.getModel('faux-thinker');
-
-console.log(thinker?.reasoning);
-console.log(registration.getPendingResponseCount());
-console.log(registration.state.callCount);
-registration.unregister();
-multiModel.unregister();
-```
-
-Notes:
-- Responses are consumed from a queue in request start order.
-- If the queue is empty, the faux provider returns an assistant error message with `errorMessage: "No more faux responses queued"`.
-- Use `registration.setResponses([...])` to replace the remaining queue and `registration.appendResponses([...])` to add more responses.
-- `registration.models` exposes all registered faux models. `registration.getModel()` returns the first one, and `registration.getModel(id)` returns a specific one.
-- Use `fauxAssistantMessage(...)` for scripted assistant replies. Use `fauxText(...)`, `fauxThinking(...)`, and `fauxToolCall(...)` to build content blocks without filling in low-level fields manually.
-- `registration.unregister()` removes the temporary provider from the global API registry.
-- Usage is estimated at roughly 1 token per 4 characters. When `sessionId` is present and `cacheRetention` is not `"none"`, prompt cache reads and writes are simulated automatically.
-- Tool call arguments stream incrementally via `toolcall_delta` chunks.
-- By default, each streamed chunk is emitted on its own microtask. Set `tokensPerSecond` to pace chunk delivery in real time.
-- The intended use is one deterministic scripted flow per registration. If you need independent concurrent flows, register separate faux providers.
-
-### Providers and Models
-
-A **provider** offers models through a specific API. For example:
-- **Anthropic** models use the `anthropic-messages` API
-- **Google** models use the `google-generative-ai` API
-- **OpenAI** models use the `openai-responses` API
-- **Mistral** models use the `mistral-conversations` API
-- **xAI, Cerebras, Groq, NVIDIA NIM, Together AI, etc.** models use the `openai-completions` API (OpenAI-compatible)
-
-### Querying Providers and Models
-
-```typescript
-import { getProviders, getModels, getModel } from '@earendil-works/pi-ai';
-
-// Get all available providers
-const providers = getProviders();
-console.log(providers); // ['openai', 'anthropic', 'google', 'xai', 'groq', ...]
-
-// Get all models from a provider (fully typed)
-const anthropicModels = getModels('anthropic');
-for (const model of anthropicModels) {
- console.log(`${model.id}: ${model.name}`);
- console.log(` API: ${model.api}`); // 'anthropic-messages'
- console.log(` Context: ${model.contextWindow} tokens`);
- console.log(` Vision: ${model.input.includes('image')}`);
- console.log(` Reasoning: ${model.reasoning}`);
-}
-
-// Get a specific model (both provider and model ID are auto-completed in IDEs)
-const model = getModel('openai', 'gpt-4o-mini');
-console.log(`Using ${model.name} via ${model.api} API`);
-```
-
-### Custom Models
-
-You can create custom models for local inference servers or custom endpoints:
-
-```typescript
-import { Model, stream } from '@earendil-works/pi-ai';
-
-// Example: Ollama using OpenAI-compatible API
const ollamaModel: Model<'openai-completions'> = {
id: 'llama-3.1-8b',
name: 'Llama 3.1 8B (Ollama)',
@@ -852,53 +936,71 @@ const ollamaModel: Model<'openai-completions'> = {
maxTokens: 32000
};
-// Example: LiteLLM proxy with explicit compat settings
-const litellmModel: Model<'openai-completions'> = {
- id: 'gpt-4o',
- name: 'GPT-4o (via LiteLLM)',
- api: 'openai-completions',
- provider: 'litellm',
- baseUrl: 'http://localhost:4000/v1',
- reasoning: false,
- input: ['text', 'image'],
- cost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },
- contextWindow: 128000,
- maxTokens: 16384,
- compat: {
- supportsStore: false, // LiteLLM doesn't support the store field
- }
-};
+const ollama = createProvider({
+ id: 'ollama',
+ name: 'Ollama',
+ baseUrl: 'http://localhost:11434/v1',
+ // Every provider declares auth; keyless local servers resolve as configured with no key.
+ auth: { apiKey: { name: 'Ollama', resolve: async () => ({ auth: {} }) } },
+ models: [ollamaModel],
+ api: openAICompletionsApi(),
+});
-// Example: Custom endpoint with headers (bypassing Cloudflare bot detection)
-const proxyModel: Model<'anthropic-messages'> = {
- id: 'claude-sonnet-4',
- name: 'Claude Sonnet 4 (Proxied)',
- api: 'anthropic-messages',
- provider: 'custom-proxy',
- baseUrl: 'https://proxy.example.com/v1',
- reasoning: true,
- input: ['text', 'image'],
- cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
- contextWindow: 200000,
- maxTokens: 8192,
- headers: {
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
- 'X-Custom-Auth': 'bearer-token-here'
- }
-};
+const models = createModels();
+models.setProvider(ollama);
-// Use the custom model
-const response = await stream(ollamaModel, context, {
- apiKey: 'dummy' // Ollama doesn't need a real key
+await models.complete(models.getModel('ollama', 'llama-3.1-8b')!, context);
+```
+
+For providers with real keys, `envApiKeyAuth(displayName, envVars)` gives the standard behavior (stored credential wins, then the first set env var):
+
+```typescript
+const proxy = createProvider({
+ id: 'my-proxy',
+ auth: { apiKey: envApiKeyAuth('My proxy API key', ['MY_PROXY_API_KEY']) },
+ models: [/* ... */],
+ api: openAICompletionsApi(),
});
```
-Some OpenAI-compatible servers do not understand the `developer` role used for reasoning-capable models. For those providers, set `compat.supportsDeveloperRole` to `false` so the system prompt is sent as a `system` message instead. If the server also does not support `reasoning_effort`, set `compat.supportsReasoningEffort` to `false` too.
+Mixed-API providers pass a map keyed by `model.api`; each model dispatches to its API's implementation:
+
+```typescript
+import { anthropicMessagesApi } from '@earendil-works/pi-ai/api/anthropic-messages.lazy';
+import { openAIResponsesApi } from '@earendil-works/pi-ai/api/openai-responses.lazy';
+
+const gateway = createProvider({
+ id: 'my-gateway',
+ auth: { apiKey: envApiKeyAuth('Gateway key', ['GATEWAY_API_KEY']) },
+ models: [/* models with api: 'anthropic-messages' or 'openai-responses' */],
+ api: {
+ 'anthropic-messages': anthropicMessagesApi(),
+ 'openai-responses': openAIResponsesApi(),
+ },
+});
+```
+
+Dynamic model lists use `refreshModels`; the provider lists empty until the first `models.refresh()`:
+
+```typescript
+const llamacpp = createProvider({
+ id: 'llamacpp',
+ auth: { apiKey: { name: 'llama.cpp', resolve: async () => ({ auth: {} }) } },
+ models: [],
+ refreshModels: async () => fetchModelsFromServer('http://localhost:8080'),
+ api: openAICompletionsApi(),
+});
+
+models.setProvider(llamacpp);
+await models.refresh('llamacpp');
+```
+
+Custom models can carry `headers` (e.g. proxies behind bot detection) and `compat` flags — see [OpenAI Compatibility Settings](#openai-compatibility-settings).
+
+Some OpenAI-compatible servers do not understand the `developer` role used for reasoning-capable models. For those providers, set `compat.supportsDeveloperRole` to `false` so the system prompt is sent as a `system` message instead. If the server also does not support `reasoning_effort`, set `compat.supportsReasoningEffort` to `false` too. This commonly applies to Ollama, vLLM, SGLang, and similar OpenAI-compatible servers.
Use model-level `thinkingLevelMap` to describe model-specific thinking controls. Keys are pi thinking levels (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`). Missing keys use provider defaults, string values are sent to the provider, and `null` marks a level unsupported.
-This commonly applies to Ollama, vLLM, SGLang, and similar OpenAI-compatible servers. You can set `compat` at the provider level or per model.
-
```typescript
const ollamaReasoningModel: Model<'openai-completions'> = {
id: 'gpt-oss:20b',
@@ -925,6 +1027,36 @@ const ollamaReasoningModel: Model<'openai-completions'> = {
};
```
+### Calling API Implementations Directly
+
+The API implementations are importable on their own. Each module exports exactly `stream` and `streamSimple` with that API's full option typing. Direct calls bypass provider auth — pass `apiKey` explicitly:
+
+```typescript
+import { stream } from '@earendil-works/pi-ai/api/anthropic-messages';
+
+const s = stream(claudeModel, context, {
+ apiKey: process.env.ANTHROPIC_API_KEY,
+ thinkingEnabled: true,
+ thinkingBudgetTokens: 2048,
+});
+```
+
+Built-in API implementations live under `./api/`:
+
+| API id | Options type |
+|--------|--------------|
+| `anthropic-messages` | `AnthropicOptions` |
+| `openai-completions` | `OpenAICompletionsOptions` |
+| `openai-responses` | `OpenAIResponsesOptions` |
+| `openai-codex-responses` | `OpenAICodexResponsesOptions` |
+| `azure-openai-responses` | `AzureOpenAIResponsesOptions` |
+| `google-generative-ai` | `GoogleOptions` |
+| `google-vertex` | `GoogleVertexOptions` |
+| `mistral-conversations` | `MistralOptions` |
+| `bedrock-converse-stream` | `BedrockOptions` |
+
+Importing an implementation module loads its SDK. The `./api/.lazy` wrappers (used by the provider factories) defer that load to the first request when the runtime or bundler supports dynamic import chunking. Legacy raw API subpaths from older releases (`./anthropic`, `./google`, `./mistral`, `./openai-completions`, ...) were removed; use `@earendil-works/pi-ai/api/`.
+
### OpenAI Compatibility Settings
The `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for a small set of known OpenAI-compatible providers (Cerebras, xAI, Chutes, DeepSeek, NVIDIA NIM, Together AI, zAi, OpenCode, Cloudflare Workers AI, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field. For `openai-responses` models, the compat field supports Responses-specific flags.
@@ -942,7 +1074,8 @@ interface OpenAICompletionsCompat {
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
- thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses enable_thinking, 'qwen' uses enable_thinking, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai)
+ thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'chat-template' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses thinking: { type }, 'qwen' uses enable_thinking, 'chat-template' uses configurable chat_template_kwargs, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking and preserve_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai)
+ chatTemplateKwargs?: Record; // chat_template_kwargs values; use $var for pi-controlled thinking values
cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content
openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {})
vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {})
@@ -961,30 +1094,97 @@ If `compat` is not set, the library falls back to URL-based detection. If `compa
- **Custom inference servers**: May use non-standard field names
- **Self-hosted endpoints**: May have different feature support
-### Type Safety
+## Faux Provider for Tests
-Models are typed by their API, which keeps the model metadata accurate. Provider-specific option types are enforced when you call the provider functions directly. The generic `stream` and `complete` functions accept `StreamOptions` with additional provider fields.
+`fauxProvider()` builds an in-memory provider with scripted responses for tests and demos:
```typescript
-import { streamAnthropic, type AnthropicOptions } from '@earendil-works/pi-ai';
+import {
+ createModels,
+ fauxAssistantMessage,
+ fauxProvider,
+ fauxText,
+ fauxThinking,
+ fauxToolCall,
+} from '@earendil-works/pi-ai';
-// TypeScript knows this is an Anthropic model
-const claude = getModel('anthropic', 'claude-sonnet-4-20250514');
+const faux = fauxProvider({
+ tokensPerSecond: 50 // optional
+});
-const options: AnthropicOptions = {
- thinkingEnabled: true,
- thinkingBudgetTokens: 2048
+const models = createModels();
+models.setProvider(faux.provider);
+
+const model = faux.getModel();
+const context = {
+ messages: [{ role: 'user', content: 'Summarize package.json and then call echo', timestamp: Date.now() }]
};
-await streamAnthropic(claude, context, options);
+faux.setResponses([
+ fauxAssistantMessage([
+ fauxThinking('Need to inspect package metadata first.'),
+ fauxToolCall('echo', { text: 'package.json' })
+ ], { stopReason: 'toolUse' })
+]);
+
+const first = await models.complete(model, context, {
+ sessionId: 'session-1',
+ cacheRetention: 'short'
+});
+context.messages.push(first);
+
+context.messages.push({
+ role: 'toolResult',
+ toolCallId: first.content.find((block) => block.type === 'toolCall')!.id,
+ toolName: 'echo',
+ content: [{ type: 'text', text: 'package.json contents here' }],
+ isError: false,
+ timestamp: Date.now()
+});
+
+faux.setResponses([
+ fauxAssistantMessage([
+ fauxThinking('Now I can summarize the tool output.'),
+ fauxText('Here is the summary.')
+ ])
+]);
+
+const s = models.stream(model, context);
+for await (const event of s) {
+ console.log(event.type);
+}
+
+// Optional: multiple faux models for model-switching tests
+const multiModel = fauxProvider({
+ provider: 'faux-multi',
+ models: [
+ { id: 'faux-fast', reasoning: false },
+ { id: 'faux-thinker', reasoning: true }
+ ]
+});
+models.setProvider(multiModel.provider);
+const thinker = multiModel.getModel('faux-thinker');
+
+console.log(thinker?.reasoning);
+console.log(faux.getPendingResponseCount());
+console.log(faux.state.callCount);
```
+Notes:
+- Responses are consumed from a queue in request start order.
+- If the queue is empty, the faux provider returns an assistant error message with `errorMessage: "No more faux responses queued"`.
+- Use `faux.setResponses([...])` to replace the remaining queue and `faux.appendResponses([...])` to add more responses.
+- `faux.models` exposes all faux models. `faux.getModel()` returns the first one, and `faux.getModel(id)` returns a specific one.
+- Use `fauxAssistantMessage(...)` for scripted assistant replies. Use `fauxText(...)`, `fauxThinking(...)`, and `fauxToolCall(...)` to build content blocks without filling in low-level fields manually.
+- Usage is estimated at roughly 1 token per 4 characters. When `sessionId` is present and `cacheRetention` is not `"none"`, prompt cache reads and writes are simulated automatically.
+- Tool call arguments stream incrementally via `toolcall_delta` chunks.
+- By default, each streamed chunk is emitted on its own microtask. Set `tokensPerSecond` to pace chunk delivery in real time.
+- The intended use is one deterministic scripted flow per handle. If you need independent concurrent flows, create separate faux providers with distinct `provider` ids.
+
## Cross-Provider Handoffs
The library supports seamless handoffs between different LLM providers within the same conversation. This allows you to switch models mid-conversation while preserving context, including thinking blocks, tool calls, and tool results.
-### How It Works
-
When messages from one provider are sent to a different provider, the library automatically transforms them for compatibility:
- **User and tool result messages** are passed through unchanged
@@ -992,98 +1192,86 @@ When messages from one provider are sent to a different provider, the library au
- **Assistant messages from different providers** have their thinking blocks converted to text with `` tags
- **Tool calls and regular text** are preserved unchanged
-### Example: Multi-Provider Conversation
-
```typescript
-import { getModel, complete, Context } from '@earendil-works/pi-ai';
+import { createModels, type Context } from '@earendil-works/pi-ai';
+import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic';
+import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';
+import { googleProvider } from '@earendil-works/pi-ai/providers/google';
+
+const models = createModels();
+models.setProvider(anthropicProvider());
+models.setProvider(openaiProvider());
+models.setProvider(googleProvider());
+
+const context: Context = { messages: [] };
// Start with Claude
-const claude = getModel('anthropic', 'claude-sonnet-4-20250514');
-const context: Context = {
- messages: []
-};
-
-context.messages.push({ role: 'user', content: 'What is 25 * 18?' });
-const claudeResponse = await complete(claude, context, {
- thinkingEnabled: true
-});
-context.messages.push(claudeResponse);
+const claude = models.getModel('anthropic', 'claude-sonnet-4-5')!;
+context.messages.push({ role: 'user', content: 'What is 25 * 18?', timestamp: Date.now() });
+context.messages.push(await models.completeSimple(claude, context, { reasoning: 'medium' }));
// Switch to GPT-5 - it will see Claude's thinking as tagged text
-const gpt5 = getModel('openai', 'gpt-5-mini');
-context.messages.push({ role: 'user', content: 'Is that calculation correct?' });
-const gptResponse = await complete(gpt5, context);
-context.messages.push(gptResponse);
+const gpt5 = models.getModel('openai', 'gpt-5-mini')!;
+context.messages.push({ role: 'user', content: 'Is that calculation correct?', timestamp: Date.now() });
+context.messages.push(await models.complete(gpt5, context));
// Switch to Gemini
-const gemini = getModel('google', 'gemini-2.5-flash');
-context.messages.push({ role: 'user', content: 'What was the original question?' });
-const geminiResponse = await complete(gemini, context);
+const gemini = models.getModel('google', 'gemini-2.5-flash')!;
+context.messages.push({ role: 'user', content: 'What was the original question?', timestamp: Date.now() });
+const geminiResponse = await models.complete(gemini, context);
```
-### Provider Compatibility
-
-All providers can handle messages from other providers, including:
-- Text content
-- Tool calls and tool results (including images in tool results)
-- Thinking/reasoning blocks (transformed to tagged text for cross-provider compatibility)
-- Aborted messages with partial content
-
-This enables flexible workflows where you can:
-- Start with a fast model for initial responses
-- Switch to a more capable model for complex reasoning
-- Use specialized models for specific tasks
-- Maintain conversation continuity across provider outages
+All providers can handle messages from other providers — text, tool calls and results (including images), thinking blocks (transformed to tagged text), and aborted messages with partial content. This enables flexible workflows: start with a fast model, switch to a more capable one for complex reasoning, or maintain continuity across provider outages.
## Context Serialization
The `Context` object can be easily serialized and deserialized using standard JSON methods, making it simple to persist conversations, implement chat history, or transfer contexts between services:
```typescript
-import { Context, getModel, complete } from '@earendil-works/pi-ai';
-
-// Create and use a context
const context: Context = {
systemPrompt: 'You are a helpful assistant.',
messages: [
- { role: 'user', content: 'What is TypeScript?' }
+ { role: 'user', content: 'What is TypeScript?', timestamp: Date.now() }
]
};
-const model = getModel('openai', 'gpt-4o-mini');
-const response = await complete(model, context);
+const model = models.getModel('openai', 'gpt-4o-mini')!;
+const response = await models.complete(model, context);
context.messages.push(response);
// Serialize the entire context
const serialized = JSON.stringify(context);
-console.log('Serialized context size:', serialized.length, 'bytes');
// Save to database, localStorage, file, etc.
localStorage.setItem('conversation', serialized);
// Later: deserialize and continue the conversation
const restored: Context = JSON.parse(localStorage.getItem('conversation')!);
-restored.messages.push({ role: 'user', content: 'Tell me more about its type system' });
+restored.messages.push({ role: 'user', content: 'Tell me more about its type system', timestamp: Date.now() });
// Continue with any model
-const newModel = getModel('anthropic', 'claude-3-5-haiku-20241022');
-const continuation = await complete(newModel, restored);
+const newModel = models.getModel('anthropic', 'claude-3-5-haiku-20241022')!;
+const continuation = await models.complete(newModel, restored);
```
+Models are plain serializable data too — no functions or implementations attached — so persisting "which model was this conversation using" is a `JSON.stringify` away.
+
> **Note**: If the context contains images (encoded as base64 as shown in the Image Input section), those will also be serialized.
## Browser Usage
-The library supports browser environments. You must pass the API key explicitly since environment variables are not available in browsers:
+The library supports browser environments. The core entrypoint and provider factories are side-effect free and bundle cleanly. Environment variables are not available in browsers, so pass API keys explicitly — or inject a `CredentialStore` (e.g. localStorage-backed) and let provider auth resolve from stored credentials:
```typescript
-import { getModel, complete } from '@earendil-works/pi-ai';
+import { createModels } from '@earendil-works/pi-ai';
+import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic';
-// API key must be passed explicitly in browser
-const model = getModel('anthropic', 'claude-3-5-haiku-20241022');
+const models = createModels();
+models.setProvider(anthropicProvider());
-const response = await complete(model, {
- messages: [{ role: 'user', content: 'Hello!' }]
+const model = models.getModel('anthropic', 'claude-3-5-haiku-20241022')!;
+const response = await models.complete(model, {
+ messages: [{ role: 'user', content: 'Hello!', timestamp: Date.now() }]
}, {
apiKey: 'your-api-key'
});
@@ -1091,69 +1279,75 @@ const response = await complete(model, {
> **Security Warning**: Exposing API keys in frontend code is dangerous. Anyone can extract and abuse your keys. Only use this approach for internal tools or demos. For production applications, use a backend proxy that keeps your API keys secure.
-### Browser Compatibility Notes
+Browser compatibility notes:
-- Amazon Bedrock (`bedrock-converse-stream`) is not supported in browser environments.
-- OAuth login flows are not supported in browser environments. Use the `@earendil-works/pi-ai/oauth` entry point in Node.js.
-- In browser builds, Bedrock can still appear in model lists. Calls to Bedrock models fail at runtime.
+- Amazon Bedrock (`bedrock-converse-stream`) is not supported in browser environments. It can still appear in model lists; calls fail at runtime.
+- OAuth login flows are Node-only. They are lazy-loaded behind bundler-opaque imports, so registering an OAuth-capable provider does not pull Node-only code into a browser bundle — only actually logging in would.
- Use a server-side proxy or backend service if you need Bedrock or OAuth-based auth from a web app.
-### Environment Variables (Node.js only)
+## Bundling and Tree Shaking
-In Node.js environments, you can set environment variables to avoid passing API keys:
-
-| Provider | Environment Variable(s) |
-|----------|------------------------|
-| OpenAI | `OPENAI_API_KEY` |
-| Ant Ling | `ANT_LING_API_KEY` |
-| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` (e.g. `https://{resource}.openai.azure.com`) or `AZURE_OPENAI_RESOURCE_NAME`. Supports `*.openai.azure.com` and `*.cognitiveservices.azure.com`; root endpoints auto-normalize to `/openai/v1`. Optional: `AZURE_OPENAI_API_VERSION` (default `v1`), `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. |
-| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` |
-| DeepSeek | `DEEPSEEK_API_KEY` |
-| NVIDIA NIM | `NVIDIA_API_KEY` |
-| Google | `GEMINI_API_KEY` |
-| Vertex AI | `GOOGLE_CLOUD_API_KEY` or `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |
-| Mistral | `MISTRAL_API_KEY` |
-| Groq | `GROQ_API_KEY` |
-| Cerebras | `CEREBRAS_API_KEY` |
-| Cloudflare AI Gateway | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_GATEWAY_ID` |
-| Cloudflare Workers AI | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` |
-| xAI | `XAI_API_KEY` |
-| Fireworks | `FIREWORKS_API_KEY` |
-| Together AI | `TOGETHER_API_KEY` |
-| OpenRouter | `OPENROUTER_API_KEY` |
-| Vercel AI Gateway | `AI_GATEWAY_API_KEY` |
-| zAI | `ZAI_API_KEY` |
-| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` |
-| MiniMax | `MINIMAX_API_KEY` |
-| OpenCode Zen / OpenCode Go | `OPENCODE_API_KEY` |
-| Kimi For Coding | `KIMI_API_KEY` |
-| Xiaomi MiMo (API billing) | `XIAOMI_API_KEY` |
-| Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` |
-| Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` |
-| Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` |
-| GitHub Copilot | `COPILOT_GITHUB_TOKEN` |
-
-When set, the library automatically uses these keys:
+For small bundles, import only the providers you need:
```typescript
-// Uses OPENAI_API_KEY from environment
-const model = getModel('openai', 'gpt-4o-mini');
-const response = await complete(model, context);
+import { createModels } from '@earendil-works/pi-ai';
+import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';
-// Or override with explicit key
-const response = await complete(model, context, {
- apiKey: 'sk-different-key'
-});
+const models = createModels();
+models.setProvider(openaiProvider());
```
+Rules:
+
+- `@earendil-works/pi-ai` is the core entrypoint and does not import built-in catalogs, provider factories, or SDK implementations.
+- `@earendil-works/pi-ai/providers/` imports that provider's catalog and lazy API wrapper only.
+- `@earendil-works/pi-ai/providers/all` imports every built-in provider factory and all catalogs. Use it only when you want the full built-in set.
+- With code splitting, provider SDKs stay in lazy chunks and load on first request.
+- Without code splitting, bundlers fold reachable lazy API implementations into the single bundle. A single-provider bundle then includes that provider's SDK; `providers/all` includes all statically visible SDKs. Bedrock is the exception: its AWS SDK implementation is loaded through a bundler-opaque Node-only import.
+- Importing `@earendil-works/pi-ai/api/` directly loads that API implementation and its SDK immediately.
+
+Avoid `@earendil-works/pi-ai/compat` in new bundled apps; it preserves the old global API and imports the full built-in catalog surface.
+
+For single-file Node ESM bundles, some SDK dependencies may still use dynamic CommonJS `require()` internally. If you see errors such as `Dynamic require of "child_process" is not supported`, add a Node `require` shim to the bundle. With esbuild:
+
+```bash
+esbuild app.js --bundle --platform=node --format=esm \
+ --banner:js='import { createRequire } from "module";const require = createRequire(import.meta.url);' \
+ --outfile=app.bundle.js
+```
+
+This is only for Node bundles; it is not a browser or Cloudflare Workers workaround.
+
+Bedrock is Node-only. Add it like any other provider:
+
+```typescript
+import { createModels } from '@earendil-works/pi-ai';
+import { amazonBedrockProvider } from '@earendil-works/pi-ai/providers/amazon-bedrock';
+
+const models = createModels();
+models.setProvider(amazonBedrockProvider());
+```
+
+In normal Node package usage and code-split bundles, Bedrock loads its AWS SDK implementation lazily. For a standalone single-file bundle that must include Bedrock support, register the implementation module explicitly:
+
+```typescript
+import { setBedrockProviderModule } from '@earendil-works/pi-ai/api/bedrock-converse-stream.lazy';
+import { bedrockProviderModule } from '@earendil-works/pi-ai/bedrock-provider';
+
+setBedrockProviderModule(bedrockProviderModule);
+```
+
+That explicit override bundles the AWS SDK. Without it, Bedrock's opaque runtime import expects the package's Bedrock implementation file to be available at runtime.
+
### Provider-Scoped Environment Overrides
-Pass `env` in stream options to scope provider configuration to a request. Values in `env` are used before process environment variables for API key discovery and provider configuration such as Cloudflare account IDs, Azure OpenAI settings, Vertex project/location, Bedrock settings, `PI_CACHE_RETENTION`, and `HTTP_PROXY`/`HTTPS_PROXY`.
+Pass `env` in stream options to scope provider configuration to a request. Values in `env` are used before process environment variables for provider auth and configuration such as Cloudflare account IDs, Azure OpenAI settings, Vertex project/location, Bedrock settings, `PI_CACHE_RETENTION`, and `HTTP_PROXY`/`HTTPS_PROXY`.
```typescript
-const model = getModel('cloudflare-ai-gateway', 'workers-ai/@cf/moonshotai/kimi-k2.6');
+const models = builtinModels();
+const model = models.getModel('cloudflare-ai-gateway', 'workers-ai/@cf/moonshotai/kimi-k2.6')!;
-const response = await complete(model, context, {
+const response = await models.complete(model, context, {
env: {
CLOUDFLARE_API_KEY: '...',
CLOUDFLARE_ACCOUNT_ID: 'account-id',
@@ -1164,24 +1358,47 @@ const response = await complete(model, context, {
Use this when one process needs different provider settings per request, or when ambient environment variables should not leak into a provider call.
-### Checking Environment Variables
-
-```typescript
-import { getEnvApiKey } from '@earendil-works/pi-ai';
-
-// Check if an API key is set in environment variables
-const key = getEnvApiKey('openai'); // checks OPENAI_API_KEY
-```
-
## OAuth Providers
-Several providers require OAuth authentication instead of static API keys:
+Several providers support OAuth authentication instead of static API keys:
- **Anthropic** (Claude Pro/Max subscription)
- **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)
- **GitHub Copilot** (Copilot subscription)
-For paid Cloud Code Assist subscriptions, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` to your project ID.
+Each of these providers carries an `OAuthAuth` on `provider.auth.oauth` with three operations: `login(callbacks)` runs the interactive flow and returns a credential, `refresh(credential)` exchanges the refresh token, and `toAuth(credential)` derives request auth (GitHub Copilot's per-account base URL comes from here). Refresh is automatic: `models.getAuth()` and the request paths refresh expired tokens under a credential-store lock, so concurrent requests and processes cannot double-refresh.
+
+```typescript
+import { createModels } from '@earendil-works/pi-ai';
+import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic';
+
+const models = createModels({ credentials: myStore }); // persistent CredentialStore
+models.setProvider(anthropicProvider());
+
+// Login: drive the flow with prompt()/notify() callbacks, persist the credential
+const provider = models.getProvider('anthropic')!;
+const credential = await provider.auth.oauth!.login({
+ prompt: async (p) => {
+ // p.type: 'text' | 'secret' | 'select' | 'manual_code'
+ // manual_code prompts race a local callback server; p.signal aborts them when the server wins
+ return await askUser(p.message);
+ },
+ notify: (event) => {
+ // event.type: 'auth_url' | 'device_code' | 'progress'
+ if (event.type === 'auth_url') console.log(`Open: ${event.url}`);
+ if (event.type === 'device_code') console.log(`Code: ${event.userCode} at ${event.verificationUri}`);
+ if (event.type === 'progress') console.log(event.message);
+ },
+});
+await myStore.modify('anthropic', async () => credential);
+
+// From here on, requests resolve and refresh the token automatically
+const model = models.getModel('anthropic', 'claude-sonnet-4-5')!;
+await models.complete(model, context);
+
+// Logout
+await myStore.delete('anthropic');
+```
### Vertex AI
@@ -1193,8 +1410,6 @@ Vertex AI models support either a Google Cloud API key or Application Default Cr
When using ADC, also set `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) and `GOOGLE_CLOUD_LOCATION`. You can also pass `project`/`location` in the call options. When using `GOOGLE_CLOUD_API_KEY`, `project` and `location` are not required.
-Example:
-
```bash
# Local (uses your user credentials)
gcloud auth application-default login
@@ -1205,23 +1420,6 @@ export GOOGLE_CLOUD_LOCATION="us-central1"
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
```
-```typescript
-import { getModel, complete } from '@earendil-works/pi-ai';
-
-(async () => {
- const model = getModel('google-vertex', 'gemini-2.5-flash');
- const response = await complete(model, {
- messages: [{ role: 'user', content: 'Hello from Vertex AI' }]
- }, {
- apiKey: process.env.GOOGLE_CLOUD_API_KEY,
- });
-
- for (const block of response.content) {
- if (block.type === 'text') console.log(block.text);
- }
-})().catch(console.error);
-```
-
Official docs: [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials)
### CLI Login
@@ -1238,76 +1436,9 @@ Credentials are saved to `auth.json` in the current directory.
### Programmatic OAuth
-The library provides login and token refresh functions via the `@earendil-works/pi-ai/oauth` entry point. Credential storage is the caller's responsibility.
+The legacy flow functions remain available via the `@earendil-works/pi-ai/oauth` entry point (`loginAnthropic`, `loginOpenAICodex`, `loginGitHubCopilot`, `refreshOAuthToken`, `getOAuthApiKey`); credential storage is the caller's responsibility there. New code should prefer the provider-owned `OAuthAuth` shown above — it composes with the credential store and gets locked auto-refresh for free.
-```typescript
-import {
- // Login functions (return credentials, do not store)
- loginAnthropic,
- loginOpenAICodex,
- loginGitHubCopilot,
- loginGeminiCli,
-
- // Token management
- refreshOAuthToken, // (provider, credentials) => new credentials
- getOAuthApiKey, // (provider, credentialsMap) => { newCredentials, apiKey } | null
-
- // Types
- type OAuthProvider,
- type OAuthCredentials,
-} from '@earendil-works/pi-ai/oauth';
-```
-
-### Login Flow Example
-
-```typescript
-import { loginGitHubCopilot } from '@earendil-works/pi-ai/oauth';
-import { writeFileSync } from 'fs';
-
-const credentials = await loginGitHubCopilot({
- onAuth: (url, instructions) => {
- console.log(`Open: ${url}`);
- if (instructions) console.log(instructions);
- },
- onPrompt: async (prompt) => {
- return await getUserInput(prompt.message);
- },
- onProgress: (message) => console.log(message)
-});
-
-// Store credentials yourself
-const auth = { 'github-copilot': { type: 'oauth', ...credentials } };
-writeFileSync('auth.json', JSON.stringify(auth, null, 2));
-```
-
-### Using OAuth Tokens
-
-Use `getOAuthApiKey()` to get an API key, automatically refreshing if expired:
-
-```typescript
-import { getModel, complete } from '@earendil-works/pi-ai';
-import { getOAuthApiKey } from '@earendil-works/pi-ai/oauth';
-import { readFileSync, writeFileSync } from 'fs';
-
-// Load your stored credentials
-const auth = JSON.parse(readFileSync('auth.json', 'utf-8'));
-
-// Get API key (refreshes if expired)
-const result = await getOAuthApiKey('github-copilot', auth);
-if (!result) throw new Error('Not logged in');
-
-// Save refreshed credentials
-auth['github-copilot'] = { type: 'oauth', ...result.newCredentials };
-writeFileSync('auth.json', JSON.stringify(auth, null, 2));
-
-// Use the API key
-const model = getModel('github-copilot', 'gpt-4o');
-const response = await complete(model, {
- messages: [{ role: 'user', content: 'Hello!' }]
-}, { apiKey: result.apiKey });
-```
-
-### Provider Notes
+Provider notes:
**OpenAI Codex**: Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options. You can set `transport` in stream options to `"sse"`, `"websocket"`, or `"auto"` for Codex Responses transport selection. When using WebSocket with a `sessionId`, connections are reused per session and expire after 5 minutes of inactivity.
@@ -1315,45 +1446,67 @@ const response = await complete(model, {
**GitHub Copilot**: If you get "The requested model is not supported" error, enable the model manually in VS Code: open Copilot Chat, click the model selector, select the model (warning icon), and click "Enable".
+## Migrating from the Old Global API
+
+Older versions exposed a global API: `stream()`/`complete()` dispatching on `model.api` via a global registry, sync `getModel()`/`getModels()`/`getProviders()` catalog reads, `registerApiProvider()`, `getEnvApiKey()`, and per-API lazy stream functions. That surface lives unchanged on the **compat entrypoint**:
+
+```typescript
+// Before
+import { getModel, complete } from '@earendil-works/pi-ai';
+
+// After (verbatim behavior, one import-path change)
+import { getModel, complete } from '@earendil-works/pi-ai/compat';
+```
+
+Compat is a strict superset of the root entrypoint, so a file can switch its import path wholesale. It will be removed in a future release; migrate to `createModels()` + provider factories:
+
+| Old | New |
+|-----|-----|
+| `getModel('openai', 'gpt-4o-mini')` | `models.getModel('openai', 'gpt-4o-mini')` or `getBuiltinModel()` from `providers/all` |
+| `getModels('anthropic')` / `getProviders()` | `models.getModels('anthropic')` / `models.getProviders()` or `getBuiltin*` |
+| `stream(model, ctx, opts)` (env-key injection) | `models.stream(model, ctx, opts)` (provider auth resolution) |
+| `registerApiProvider({ api, stream, streamSimple })` | `createProvider({ id, auth, models, api })` + `models.setProvider()` |
+| `getEnvApiKey('openai')` | `await models.getAuth(model)` |
+| `streamAnthropic(model, ctx, opts)` | `stream` from `@earendil-works/pi-ai/api/anthropic-messages`, or a provider in a collection |
+| `registerFauxProvider()` | `fauxProvider()` + `models.setProvider()` |
+
## Development
### Adding a New Provider
-Adding a new LLM provider requires changes across multiple files. This checklist covers all necessary steps:
+Adding a new LLM provider requires changes across multiple files. The layered layout: API implementations live in `src/api/`, provider factories in `src/providers/`, generated catalogs in `src/providers/.models.ts`. This checklist covers all necessary steps:
#### 1. Core Types (`src/types.ts`)
-- Add the API identifier to `KnownApi` (for example `"bedrock-converse-stream"`)
-- Create an options interface extending `StreamOptions` (for example `BedrockOptions`)
+- Add the API identifier to `KnownApi` (for example `"bedrock-converse-stream"`), if it is a new API
- Add the provider name to `KnownProvider` (for example `"amazon-bedrock"`)
+- Add the options type to `ApiOptionsMap`
-#### 2. Provider Implementation (`src/providers/`)
+#### 2. API Implementation (`src/api/.ts`, only for a new API)
-Create a new provider file (for example `amazon-bedrock.ts`) that exports:
+Create a new API implementation file (for example `bedrock-converse-stream.ts`) that exports exactly `stream` and `streamSimple`, plus:
-- `stream()` function returning `AssistantMessageEventStream`
-- `streamSimple()` for `SimpleStreamOptions` mapping
-- Provider-specific options interface
+- An options interface extending `StreamOptions` (for example `BedrockOptions`)
- Message conversion functions to transform `Context` to provider format
- Tool conversion if the provider supports tools
- Response parsing to emit standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`)
-#### 3. API Registry Integration (`src/providers/register-builtins.ts`)
+Add a lazy wrapper `src/api/.lazy.ts` (`Api()` via `lazyApi()`) so providers can reference the implementation without importing its SDK. Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai`.
-- Register the API with `registerApiProvider()`
-- Add a package subpath export in `package.json` for the provider module (`./dist/providers/.js`)
-- Add lazy loader wrappers in `src/providers/register-builtins.ts`, do not statically import provider implementation modules there
-- Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai`
-- Add credential detection in `env-api-keys.ts` for the new provider
-- Ensure `streamSimple` handles auth lookup via `getEnvApiKey()` or provider-specific auth
-
-#### 4. Model Generation (`scripts/generate-models.ts`, `scripts/generate-image-models.ts`)
+#### 3. Model Generation (`scripts/generate-models.ts`, `scripts/generate-image-models.ts`)
- Add logic to fetch and parse models from the provider's source (e.g., models.dev API)
-- Map chat/tool-capable provider model data to the standardized `Model` interface via `scripts/generate-models.ts`
+- Map chat/tool-capable provider model data to the standardized `Model` interface via `scripts/generate-models.ts`; regeneration emits `src/providers/.models.ts` and the aggregator
- Map image-generation provider model data to the standardized `ImagesModel` interface via `scripts/generate-image-models.ts`
- Handle provider-specific quirks (pricing format, capability flags, model ID transformations)
+#### 4. Provider Factory (`src/providers/.ts`)
+
+- `createProvider()` wiring catalog + auth + the lazy API wrapper
+- Auth: `envApiKeyAuth` for standard key providers, a custom `ApiKeyAuth` for ambient auth (AWS profiles, ADC), `lazyOAuth` where an OAuth flow exists
+- Register the factory in `src/providers/all.ts`
+- If it is a new API: register it in the builtin list in `src/compat.ts` and add the package subpath export in `package.json`
+
#### 5. Tests (`test/`)
Create or update test files to cover the new provider:
@@ -1369,6 +1522,7 @@ Create or update test files to cover the new provider:
- `image-tool-result.test.ts` - Images in tool results
- `total-tokens.test.ts` - Token counting accuracy
- `cross-provider-handoff.test.ts` - Cross-provider context replay
+- `providers.test.ts` - Provider listing and auth resolution
For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (for example GPT and Claude), add at least one pair per family.
diff --git a/packages/ai/package.json b/packages/ai/package.json
index 11e4261b..20858b7a 100644
--- a/packages/ai/package.json
+++ b/packages/ai/package.json
@@ -1,46 +1,31 @@
{
"name": "@earendil-works/pi-ai",
- "version": "0.79.6",
+ "version": "0.80.2",
"description": "Unified LLM API with automatic model discovery and provider configuration",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+ "sideEffects": [
+ "./dist/compat.js",
+ "./dist/images.js",
+ "./dist/providers/images/register-builtins.js"
+ ],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
- "./anthropic": {
- "types": "./dist/providers/anthropic.d.ts",
- "import": "./dist/providers/anthropic.js"
+ "./compat": {
+ "types": "./dist/compat.d.ts",
+ "import": "./dist/compat.js"
},
- "./azure-openai-responses": {
- "types": "./dist/providers/azure-openai-responses.d.ts",
- "import": "./dist/providers/azure-openai-responses.js"
+ "./providers/*": {
+ "types": "./dist/providers/*.d.ts",
+ "import": "./dist/providers/*.js"
},
- "./google": {
- "types": "./dist/providers/google.d.ts",
- "import": "./dist/providers/google.js"
- },
- "./google-vertex": {
- "types": "./dist/providers/google-vertex.d.ts",
- "import": "./dist/providers/google-vertex.js"
- },
- "./mistral": {
- "types": "./dist/providers/mistral.d.ts",
- "import": "./dist/providers/mistral.js"
- },
- "./openai-codex-responses": {
- "types": "./dist/providers/openai-codex-responses.d.ts",
- "import": "./dist/providers/openai-codex-responses.js"
- },
- "./openai-completions": {
- "types": "./dist/providers/openai-completions.d.ts",
- "import": "./dist/providers/openai-completions.js"
- },
- "./openai-responses": {
- "types": "./dist/providers/openai-responses.d.ts",
- "import": "./dist/providers/openai-responses.js"
+ "./api/*": {
+ "types": "./dist/api/*.d.ts",
+ "import": "./dist/api/*.js"
},
"./oauth": {
"types": "./dist/oauth.d.ts",
@@ -69,9 +54,10 @@
"dependencies": {
"@anthropic-ai/sdk": "0.91.1",
"@aws-sdk/client-bedrock-runtime": "3.1048.0",
- "@smithy/node-http-handler": "4.7.3",
"@google/genai": "1.52.0",
- "@mistralai/mistralai": "2.2.1",
+ "@mistralai/mistralai": "2.2.6",
+ "@opentelemetry/api": "1.9.0",
+ "@smithy/node-http-handler": "4.7.3",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"openai": "6.26.0",
@@ -101,6 +87,6 @@
"devDependencies": {
"@types/node": "24.12.4",
"canvas": "3.2.3",
- "vitest": "3.2.4"
+ "vitest": "4.1.9"
}
}
diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts
index 3d75a82e..3b0677b6 100644
--- a/packages/ai/scripts/generate-models.ts
+++ b/packages/ai/scripts/generate-models.ts
@@ -1,6 +1,6 @@
#!/usr/bin/env node
-import { writeFileSync } from "fs";
+import { readdirSync, rmSync, writeFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import {
@@ -8,7 +8,7 @@ import {
CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL,
CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL,
CLOUDFLARE_WORKERS_AI_BASE_URL,
-} from "../src/providers/cloudflare.ts";
+} from "../src/api/cloudflare.ts";
import type { AnthropicMessagesCompat, Api, KnownProvider, Model, OpenAICompletionsCompat } from "../src/types.ts";
const __filename = fileURLToPath(import.meta.url);
@@ -164,6 +164,14 @@ const ZAI_GLM52_THINKING_LEVEL_MAP = {
high: "high",
xhigh: "max",
} as const;
+const OPENCODE_GO_GLM52_THINKING_LEVEL_MAP = {
+ off: null,
+ minimal: null,
+ low: null,
+ medium: null,
+ high: "high",
+ xhigh: "max",
+} as const;
const EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS = new Set([
"github-copilot:claude-haiku-4.5",
"github-copilot:claude-sonnet-4",
@@ -269,10 +277,149 @@ function isAnthropicTemperatureUnsupportedModel(modelId: string): boolean {
return id.includes("opus-4-7") || id.includes("opus-4.7") || id.includes("opus-4-8") || id.includes("opus-4.8");
}
+const OPENAI_COMPLETIONS_DEFAULT_COMPAT = {
+ supportsStore: true,
+ supportsDeveloperRole: true,
+ supportsReasoningEffort: true,
+ supportsUsageInStreaming: true,
+ maxTokensField: "max_completion_tokens",
+ requiresToolResultName: false,
+ requiresAssistantAfterToolResult: false,
+ requiresThinkingAsText: false,
+ requiresReasoningContentOnAssistantMessages: false,
+ thinkingFormat: "openai",
+ openRouterRouting: {},
+ vercelGatewayRouting: {},
+ chatTemplateKwargs: {},
+ zaiToolStream: false,
+ supportsStrictMode: true,
+ sendSessionAffinityHeaders: false,
+ supportsLongCacheRetention: true,
+} satisfies Required> & {
+ cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"];
+};
+
+type OpenAICompletionsResolvedCompat = typeof OPENAI_COMPLETIONS_DEFAULT_COMPAT & {
+ cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"];
+};
+
function mergeAnthropicMessagesCompat(model: Model, compat: AnthropicMessagesCompat): void {
model.compat = { ...(model.compat as AnthropicMessagesCompat | undefined), ...compat };
}
+function detectOpenAICompletionsCompat(model: Model<"openai-completions">): OpenAICompletionsResolvedCompat {
+ const provider = model.provider;
+ const baseUrl = model.baseUrl;
+
+ const isZai =
+ provider === "zai" ||
+ provider === "zai-coding-cn" ||
+ baseUrl.includes("api.z.ai") ||
+ baseUrl.includes("open.bigmodel.cn");
+ const isTogether =
+ provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz");
+ const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot.");
+ const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai");
+ const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com");
+ const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com");
+ const isNvidia = provider === "nvidia" || baseUrl.includes("integrate.api.nvidia.com");
+ const isAntLing = provider === "ant-ling" || baseUrl.includes("api.ant-ling.com");
+ const isTogetherReasoningOnly = isTogether && TOGETHER_REASONING_ONLY_MODELS.has(model.id);
+
+ const isNonStandard =
+ isNvidia ||
+ provider === "cerebras" ||
+ baseUrl.includes("cerebras.ai") ||
+ provider === "xai" ||
+ baseUrl.includes("api.x.ai") ||
+ isTogether ||
+ baseUrl.includes("chutes.ai") ||
+ baseUrl.includes("deepseek.com") ||
+ isZai ||
+ isMoonshot ||
+ provider === "opencode" ||
+ baseUrl.includes("opencode.ai") ||
+ isCloudflareWorkersAI ||
+ isCloudflareAiGateway ||
+ isAntLing;
+
+ const useMaxTokens =
+ baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isTogether || isNvidia || isAntLing;
+
+ const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
+ const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
+ const isOpenRouterDeveloperRoleModel =
+ isOpenRouter && (model.id.startsWith("anthropic/") || model.id.startsWith("openai/"));
+ const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined;
+
+ return {
+ supportsStore: !isNonStandard,
+ supportsDeveloperRole: isOpenRouterDeveloperRoleModel || (!isNonStandard && !isOpenRouter),
+ supportsReasoningEffort:
+ !isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia && !isAntLing,
+ supportsUsageInStreaming: true,
+ maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
+ requiresToolResultName: false,
+ requiresAssistantAfterToolResult: false,
+ requiresThinkingAsText: false,
+ requiresReasoningContentOnAssistantMessages: isDeepSeek,
+ thinkingFormat: isDeepSeek
+ ? "deepseek"
+ : isZai
+ ? "zai"
+ : isTogether && !isTogetherReasoningOnly
+ ? "together"
+ : isAntLing
+ ? "ant-ling"
+ : isOpenRouter
+ ? "openrouter"
+ : "openai",
+ openRouterRouting: {},
+ vercelGatewayRouting: {},
+ chatTemplateKwargs: {},
+ zaiToolStream: false,
+ supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia,
+ ...(cacheControlFormat ? { cacheControlFormat } : {}),
+ sendSessionAffinityHeaders: false,
+ supportsLongCacheRetention: !(
+ isTogether ||
+ isCloudflareWorkersAI ||
+ isCloudflareAiGateway ||
+ isNvidia ||
+ isAntLing
+ ),
+ };
+}
+
+function isPlainEmptyObject(value: unknown): boolean {
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
+}
+
+function openAICompletionsCompatDelta(compat: OpenAICompletionsResolvedCompat): OpenAICompletionsCompat {
+ const delta: OpenAICompletionsCompat = {};
+ for (const [key, value] of Object.entries(compat)) {
+ const defaultValue = OPENAI_COMPLETIONS_DEFAULT_COMPAT[key as keyof typeof OPENAI_COMPLETIONS_DEFAULT_COMPAT];
+ if (isPlainEmptyObject(value) && isPlainEmptyObject(defaultValue)) continue;
+ if (value !== defaultValue) {
+ (delta as Record)[key] = value;
+ }
+ }
+ return delta;
+}
+
+function mergeOpenAICompletionsCompat(model: Model, compat: OpenAICompletionsCompat): void {
+ model.compat = { ...(model.compat as OpenAICompletionsCompat | undefined), ...compat };
+}
+
+function applyOpenAICompletionsCompatMetadata(model: Model): void {
+ if (model.api !== "openai-completions") return;
+ const detected = openAICompletionsCompatDelta(detectOpenAICompletionsCompat(model as Model<"openai-completions">));
+ model.compat = { ...detected, ...(model.compat as OpenAICompletionsCompat | undefined) };
+ if (Object.keys(model.compat).length === 0) {
+ delete model.compat;
+ }
+}
+
function isGemini3ProModel(modelId: string): boolean {
return /gemini-3(?:\.\d+)?-pro/.test(modelId.toLowerCase());
}
@@ -374,6 +521,15 @@ function applyThinkingLevelMetadata(model: Model): void {
// Pi's low/medium/high pass through verbatim; OpenRouter normalizes to Mercury's vocabulary.
mergeThinkingLevelMap(model, { off: null });
}
+ if (model.provider === "openrouter" && model.id === "z-ai/glm-5.2") {
+ mergeThinkingLevelMap(model, { xhigh: "xhigh" });
+ }
+ if (model.provider === "fireworks" && model.id === "accounts/fireworks/models/glm-5p2") {
+ mergeThinkingLevelMap(model, { off: "none", minimal: null, low: "high", medium: "high", xhigh: "max" });
+ }
+ if (model.provider === "opencode-go" && model.id === "glm-5.2") {
+ mergeThinkingLevelMap(model, OPENCODE_GO_GLM52_THINKING_LEVEL_MAP);
+ }
if (model.provider === "opencode-go" && model.id === "kimi-k2.6") {
// OpenCode Go exposes Kimi K2.6 thinking as on/off, not distinct effort tiers.
mergeThinkingLevelMap(model, { minimal: null, low: null, medium: null });
@@ -837,9 +993,10 @@ async function loadModelsDevData(): Promise[]> {
continue;
}
- // workers-ai/* through the gateway forwards x-session-affinity to
- // the underlying Workers AI runtime for prefix-cache routing.
- const compat = upstream === "workers-ai" ? { sendSessionAffinityHeaders: true } : undefined;
+ // Gateway passthroughs forward session affinity headers to upstreams that
+ // use them for cache/routing affinity.
+ const compat =
+ upstream === "anthropic" || upstream === "workers-ai" ? { sendSessionAffinityHeaders: true } : undefined;
models.push({
id,
@@ -948,7 +1105,7 @@ async function loadModelsDevData(): Promise[]> {
cost: {
input: m.cost?.input || 0,
output: m.cost?.output || 0,
- cacheRead: m.cost?.cache_read || 0,
+ cacheRead: m.cost?.cache_read ?? (m.cost?.input ? roundCost(m.cost.input * 0.1) : 0),
cacheWrite: m.cost?.cache_write || 0,
},
contextWindow: m.limit?.context || 4096,
@@ -1511,7 +1668,11 @@ async function generateModels() {
candidate.cost.output = 1.9;
candidate.cost.cacheRead = 0.119;
}
-
+ if (candidate.provider === "fireworks" && candidate.id === "accounts/fireworks/models/glm-5p2") {
+ candidate.api = "openai-completions";
+ candidate.baseUrl = "https://api.fireworks.ai/inference/v1";
+ candidate.compat = { supportsStore: false, supportsDeveloperRole: false };
+ }
}
@@ -2030,6 +2191,32 @@ async function generateModels() {
});
}
+ // Add "fusion" alias for openrouter/fusion. OpenRouter exposes Fusion as a
+ // router alias/plugin entry point; its model metadata does not advertise
+ // tools, but the alias resolves to a concrete model that can invoke caller
+ // tools and has the openrouter:fusion server tool auto-injected.
+ if (!allModels.some(m => m.provider === "openrouter" && m.id === "openrouter/fusion")) {
+ allModels.push({
+ id: "openrouter/fusion",
+ name: "OpenRouter: Fusion",
+ api: "openai-completions",
+ provider: "openrouter",
+ baseUrl: "https://openrouter.ai/api/v1",
+ reasoning: true,
+ input: ["text"],
+ cost: {
+ // we dont know about the costs because Fusion routes to multiple models
+ // and then charges you for the underlying used models
+ input: 0,
+ output: 0,
+ cacheRead: 0,
+ cacheWrite: 0,
+ },
+ contextWindow: 1000000,
+ maxTokens: 30000,
+ });
+ }
+
// Azure Foundry deploys these with larger context windows than OpenAI's own API,
// which caps gpt-5.4/gpt-5.5 at 272k. See models-sold-directly-by-azure docs.
const AZURE_CONTEXT_WINDOW_OVERRIDES: Record = {
@@ -2049,6 +2236,7 @@ async function generateModels() {
for (const model of allModels) {
applyThinkingLevelMetadata(model);
+ applyOpenAICompletionsCompatMetadata(model);
}
// Group by provider and deduplicate by model ID
@@ -2064,62 +2252,80 @@ async function generateModels() {
}
}
- // Generate TypeScript file
- let output = `// This file is auto-generated by scripts/generate-models.ts
+ // Generate TypeScript files: one catalog per provider plus an aggregator
+ const generatedHeader = `// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
-import type { Model } from "./types.ts";
-
-export const MODELS = {
`;
+ const catalogConstName = (providerId: string) => `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`;
- // Generate provider sections (sorted for deterministic output)
- const sortedProviderIds = Object.keys(providers).sort();
- for (const providerId of sortedProviderIds) {
- const models = providers[providerId];
- output += `\t${JSON.stringify(providerId)}: {\n`;
-
- const sortedModelIds = Object.keys(models).sort();
- for (const modelId of sortedModelIds) {
- const model = models[modelId];
- output += `\t\t"${model.id}": {\n`;
- output += `\t\t\tid: "${model.id}",\n`;
- output += `\t\t\tname: "${model.name}",\n`;
- output += `\t\t\tapi: "${model.api}",\n`;
- output += `\t\t\tprovider: "${model.provider}",\n`;
- if (model.baseUrl !== undefined) {
- output += `\t\t\tbaseUrl: "${model.baseUrl}",\n`;
- }
- if (model.headers) {
- output += `\t\t\theaders: ${JSON.stringify(model.headers)},\n`;
- }
- if (model.compat) {
- output += ` compat: ${JSON.stringify(model.compat)},
-`;
- }
- output += `\t\t\treasoning: ${model.reasoning},\n`;
- if (model.thinkingLevelMap) {
- output += `\t\t\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`;
- }
- output += `\t\t\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;
- output += `\t\t\tcost: {\n`;
- output += `\t\t\t\tinput: ${model.cost.input},\n`;
- output += `\t\t\t\toutput: ${model.cost.output},\n`;
- output += `\t\t\t\tcacheRead: ${model.cost.cacheRead},\n`;
- output += `\t\t\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
- output += `\t\t\t},\n`;
- output += `\t\t\tcontextWindow: ${model.contextWindow},\n`;
- output += `\t\t\tmaxTokens: ${model.maxTokens},\n`;
- output += `\t\t} satisfies Model<"${model.api}">,\n`;
+ function emitModel(model: Model, indent: string): string {
+ let output = `${indent}"${model.id}": {\n`;
+ output += `${indent}\tid: "${model.id}",\n`;
+ output += `${indent}\tname: "${model.name}",\n`;
+ output += `${indent}\tapi: "${model.api}",\n`;
+ output += `${indent}\tprovider: "${model.provider}",\n`;
+ if (model.baseUrl !== undefined) {
+ output += `${indent}\tbaseUrl: "${model.baseUrl}",\n`;
}
-
- output += `\t},\n`;
+ if (model.headers) {
+ output += `${indent}\theaders: ${JSON.stringify(model.headers)},\n`;
+ }
+ if (model.compat) {
+ output += `${indent}\tcompat: ${JSON.stringify(model.compat)},\n`;
+ }
+ output += `${indent}\treasoning: ${model.reasoning},\n`;
+ if (model.thinkingLevelMap) {
+ output += `${indent}\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`;
+ }
+ output += `${indent}\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;
+ output += `${indent}\tcost: {\n`;
+ output += `${indent}\t\tinput: ${model.cost.input},\n`;
+ output += `${indent}\t\toutput: ${model.cost.output},\n`;
+ output += `${indent}\t\tcacheRead: ${model.cost.cacheRead},\n`;
+ output += `${indent}\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
+ output += `${indent}\t},\n`;
+ output += `${indent}\tcontextWindow: ${model.contextWindow},\n`;
+ output += `${indent}\tmaxTokens: ${model.maxTokens},\n`;
+ output += `${indent}} satisfies Model<"${model.api}">,\n`;
+ return output;
}
- output += `} as const;
-`;
+ const sortedProviderIds = Object.keys(providers).sort();
+ const providersDir = join(packageRoot, "src/providers");
- // Write file
+ // Remove stale per-provider catalogs
+ for (const entry of readdirSync(providersDir)) {
+ if (entry.endsWith(".models.ts")) {
+ rmSync(join(providersDir, entry));
+ }
+ }
+
+ // Per-provider catalogs (sorted for deterministic output)
+ for (const providerId of sortedProviderIds) {
+ const models = providers[providerId];
+ let output = generatedHeader;
+ output += `import type { Model } from "../types.ts";\n\n`;
+ output += `export const ${catalogConstName(providerId)} = {\n`;
+ const sortedModelIds = Object.keys(models).sort();
+ for (const modelId of sortedModelIds) {
+ output += emitModel(models[modelId], "\t");
+ }
+ output += `} as const;\n`;
+ writeFileSync(join(providersDir, `${providerId}.models.ts`), output);
+ }
+ console.log(`Generated ${sortedProviderIds.length} catalogs under src/providers/`);
+
+ // Aggregator
+ let output = generatedHeader;
+ for (const providerId of sortedProviderIds) {
+ output += `import { ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`;
+ }
+ output += `\nexport const MODELS = {\n`;
+ for (const providerId of sortedProviderIds) {
+ output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`;
+ }
+ output += `} as const;\n`;
writeFileSync(join(packageRoot, "src/models.generated.ts"), output);
console.log("Generated src/models.generated.ts");
diff --git a/packages/ai/src/api-registry.ts b/packages/ai/src/api-registry.ts
deleted file mode 100644
index d86dd3e9..00000000
--- a/packages/ai/src/api-registry.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import type {
- Api,
- AssistantMessageEventStream,
- Context,
- Model,
- SimpleStreamOptions,
- StreamFunction,
- StreamOptions,
-} from "./types.ts";
-
-export type ApiStreamFunction = (
- model: Model,
- context: Context,
- options?: StreamOptions,
-) => AssistantMessageEventStream;
-
-export type ApiStreamSimpleFunction = (
- model: Model,
- context: Context,
- options?: SimpleStreamOptions,
-) => AssistantMessageEventStream;
-
-export interface ApiProvider {
- api: TApi;
- stream: StreamFunction;
- streamSimple: StreamFunction;
-}
-
-interface ApiProviderInternal {
- api: Api;
- stream: ApiStreamFunction;
- streamSimple: ApiStreamSimpleFunction;
-}
-
-type RegisteredApiProvider = {
- provider: ApiProviderInternal;
- sourceId?: string;
-};
-
-const apiProviderRegistry = new Map();
-
-function wrapStream(
- api: TApi,
- stream: StreamFunction,
-): ApiStreamFunction {
- return (model, context, options) => {
- if (model.api !== api) {
- throw new Error(`Mismatched api: ${model.api} expected ${api}`);
- }
- return stream(model as Model, context, options as TOptions);
- };
-}
-
-function wrapStreamSimple(
- api: TApi,
- streamSimple: StreamFunction,
-): ApiStreamSimpleFunction {
- return (model, context, options) => {
- if (model.api !== api) {
- throw new Error(`Mismatched api: ${model.api} expected ${api}`);
- }
- return streamSimple(model as Model, context, options);
- };
-}
-
-export function registerApiProvider(
- provider: ApiProvider,
- sourceId?: string,
-): void {
- apiProviderRegistry.set(provider.api, {
- provider: {
- api: provider.api,
- stream: wrapStream(provider.api, provider.stream),
- streamSimple: wrapStreamSimple(provider.api, provider.streamSimple),
- },
- sourceId,
- });
-}
-
-export function getApiProvider(api: Api): ApiProviderInternal | undefined {
- return apiProviderRegistry.get(api)?.provider;
-}
-
-export function getApiProviders(): ApiProviderInternal[] {
- return Array.from(apiProviderRegistry.values(), (entry) => entry.provider);
-}
-
-export function unregisterApiProviders(sourceId: string): void {
- for (const [api, entry] of apiProviderRegistry.entries()) {
- if (entry.sourceId === sourceId) {
- apiProviderRegistry.delete(api);
- }
- }
-}
-
-export function clearApiProviders(): void {
- apiProviderRegistry.clear();
-}
diff --git a/packages/ai/src/api/anthropic-messages.lazy.ts b/packages/ai/src/api/anthropic-messages.lazy.ts
new file mode 100644
index 00000000..1da2172e
--- /dev/null
+++ b/packages/ai/src/api/anthropic-messages.lazy.ts
@@ -0,0 +1,4 @@
+import type { ProviderStreams } from "../types.ts";
+import { lazyApi } from "./lazy.ts";
+
+export const anthropicMessagesApi = (): ProviderStreams => lazyApi(() => import("./anthropic-messages.ts"));
diff --git a/packages/ai/src/api/anthropic-messages.ts b/packages/ai/src/api/anthropic-messages.ts
new file mode 100644
index 00000000..03bbc26c
--- /dev/null
+++ b/packages/ai/src/api/anthropic-messages.ts
@@ -0,0 +1,1229 @@
+import Anthropic from "@anthropic-ai/sdk";
+import type {
+ CacheControlEphemeral,
+ ContentBlockParam,
+ MessageCreateParamsStreaming,
+ MessageParam,
+ RawMessageStreamEvent,
+ RefusalStopDetails,
+} from "@anthropic-ai/sdk/resources/messages.js";
+import { calculateCost } from "../models.ts";
+import type {
+ AnthropicMessagesCompat,
+ Api,
+ AssistantMessage,
+ CacheRetention,
+ Context,
+ ImageContent,
+ Message,
+ Model,
+ ProviderEnv,
+ ProviderHeaders,
+ SimpleStreamOptions,
+ StopReason,
+ StreamFunction,
+ StreamOptions,
+ TextContent,
+ ThinkingContent,
+ Tool,
+ ToolCall,
+ ToolResultMessage,
+} from "../types.ts";
+import { AssistantMessageEventStream } from "../utils/event-stream.ts";
+import { headersToRecord } from "../utils/headers.ts";
+import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.ts";
+import { getProviderEnvValue } from "../utils/provider-env.ts";
+import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
+
+import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts";
+import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.ts";
+import { transformMessages } from "./transform-messages.ts";
+
+/**
+ * Resolve cache retention preference.
+ * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility.
+ */
+function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention {
+ if (cacheRetention) {
+ return cacheRetention;
+ }
+ if (getProviderEnvValue("PI_CACHE_RETENTION", env) === "long") {
+ return "long";
+ }
+ return "short";
+}
+
+function getCacheControl(
+ model: Model<"anthropic-messages">,
+ cacheRetention?: CacheRetention,
+ env?: ProviderEnv,
+): { retention: CacheRetention; cacheControl?: CacheControlEphemeral } {
+ const retention = resolveCacheRetention(cacheRetention, env);
+ if (retention === "none") {
+ return { retention };
+ }
+ const ttl = retention === "long" && getAnthropicCompat(model).supportsLongCacheRetention ? "1h" : undefined;
+ return {
+ retention,
+ cacheControl: { type: "ephemeral", ...(ttl && { ttl }) },
+ };
+}
+
+// Stealth mode: Mimic Claude Code's tool naming exactly
+const claudeCodeVersion = "2.1.75";
+
+// Claude Code 2.x tool names (canonical casing)
+// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md
+// To update: https://github.com/badlogic/cchistory
+const claudeCodeTools = [
+ "Read",
+ "Write",
+ "Edit",
+ "Bash",
+ "Grep",
+ "Glob",
+ "AskUserQuestion",
+ "EnterPlanMode",
+ "ExitPlanMode",
+ "KillShell",
+ "NotebookEdit",
+ "Skill",
+ "Task",
+ "TaskOutput",
+ "TodoWrite",
+ "WebFetch",
+ "WebSearch",
+];
+
+const ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));
+
+// Convert tool name to CC canonical casing if it matches (case-insensitive)
+const toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name;
+const fromClaudeCodeName = (name: string, tools?: Tool[]) => {
+ if (tools && tools.length > 0) {
+ const lowerName = name.toLowerCase();
+ const matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName);
+ if (matchedTool) return matchedTool.name;
+ }
+ return name;
+};
+
+/**
+ * Convert content blocks to Anthropic API format
+ */
+function convertContentBlocks(content: (TextContent | ImageContent)[]):
+ | string
+ | Array<
+ | { type: "text"; text: string }
+ | {
+ type: "image";
+ source: {
+ type: "base64";
+ media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
+ data: string;
+ };
+ }
+ > {
+ // If only text blocks, return as concatenated string for simplicity
+ const hasImages = content.some((c) => c.type === "image");
+ if (!hasImages) {
+ return sanitizeSurrogates(content.map((c) => (c as TextContent).text).join("\n"));
+ }
+
+ // If we have images, convert to content block array
+ const blocks = content.map((block) => {
+ if (block.type === "text") {
+ return {
+ type: "text" as const,
+ text: sanitizeSurrogates(block.text),
+ };
+ }
+ return {
+ type: "image" as const,
+ source: {
+ type: "base64" as const,
+ media_type: block.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
+ data: block.data,
+ },
+ };
+ });
+
+ // If only images (no text), add placeholder text block
+ const hasText = blocks.some((b) => b.type === "text");
+ if (!hasText) {
+ blocks.unshift({
+ type: "text" as const,
+ text: "(see attached image)",
+ });
+ }
+
+ return blocks;
+}
+
+export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
+
+export type AnthropicThinkingDisplay = "summarized" | "omitted";
+
+const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
+const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
+
+function getAnthropicCompat(
+ model: Model<"anthropic-messages">,
+): Required> {
+ return {
+ supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,
+ supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
+ sendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? false,
+ supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? true,
+ supportsTemperature: model.compat?.supportsTemperature ?? true,
+ allowEmptySignature: model.compat?.allowEmptySignature ?? false,
+ };
+}
+
+export interface AnthropicOptions extends StreamOptions {
+ /**
+ * Enable extended thinking.
+ * For adaptive thinking models: the model decides when/how much to think.
+ * For older models: uses budget-based thinking with thinkingBudgetTokens.
+ * Default: undefined (thinking is omitted unless `streamSimple()` maps
+ * a simple reasoning level to this option, or callers set it explicitly).
+ */
+ thinkingEnabled?: boolean;
+ /**
+ * Token budget for extended thinking (older models only).
+ * Ignored for adaptive thinking models.
+ * Default: 1024 when `thinkingEnabled` is true and no budget is provided.
+ */
+ thinkingBudgetTokens?: number;
+ /**
+ * Effort level for adaptive thinking models.
+ * Controls how much thinking Claude allocates:
+ * - "max": Always thinks with no constraints (Opus 4.6 only)
+ * - "xhigh": Highest reasoning level (Opus 4.7+, Fable 5)
+ * - "high": Always thinks, deep reasoning
+ * - "medium": Moderate thinking, may skip for simple queries
+ * - "low": Minimal thinking, skips for simple tasks
+ * Ignored for older models.
+ * Default: omitted unless `streamSimple()` maps a simple reasoning
+ * level to this option.
+ */
+ effort?: AnthropicEffort;
+ /**
+ * Controls how thinking content is returned in API responses.
+ * - "summarized": Thinking blocks contain summarized thinking text.
+ * - "omitted": Thinking blocks return an empty thinking field; the encrypted
+ * signature still travels back for multi-turn continuity. Use for faster
+ * time-to-first-text-token when your UI does not surface thinking.
+ *
+ * Note: Anthropic's API default for Claude Opus 4.7 and Claude Mythos Preview
+ * is "omitted". We default to "summarized" here to keep behavior consistent
+ * with older Claude 4 models. Set this explicitly to "omitted" to opt in.
+ * Default: "summarized" when thinking is enabled.
+ */
+ thinkingDisplay?: AnthropicThinkingDisplay;
+ /**
+ * Whether to request the interleaved thinking beta header for non-adaptive
+ * thinking models. Adaptive thinking models have interleaved thinking built in,
+ * so the header is skipped for them regardless of this setting.
+ * Default: true.
+ */
+ interleavedThinking?: boolean;
+ /**
+ * Anthropic tool choice behavior. String values map to Anthropic's built-in
+ * choices; `{ type: "tool", name }` forces a specific tool.
+ * Default: omitted (Anthropic default behavior, currently equivalent to auto).
+ */
+ toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
+ /**
+ * Pre-built Anthropic client instance. When provided, skips internal client
+ * construction entirely. Use this to inject alternative SDK clients such as
+ * `AnthropicVertex` that shares the same messaging API.
+ */
+ client?: Anthropic;
+}
+
+function mergeHeaders(...headerSources: (ProviderHeaders | undefined)[]): ProviderHeaders {
+ const merged: ProviderHeaders = {};
+ for (const headers of headerSources) {
+ if (headers) {
+ Object.assign(merged, headers);
+ }
+ }
+ return merged;
+}
+
+function hasHeader(headers: ProviderHeaders | undefined, name: string): boolean {
+ if (!headers) return false;
+ const expected = name.toLowerCase();
+ for (const [key, value] of Object.entries(headers)) {
+ if (key.toLowerCase() === expected && value !== null && value.trim().length > 0) return true;
+ }
+ return false;
+}
+
+function assertRequestAuth(provider: string, apiKey: string | undefined, headers: ProviderHeaders | undefined): void {
+ if (apiKey) return;
+ if (
+ hasHeader(headers, "authorization") ||
+ hasHeader(headers, "x-api-key") ||
+ hasHeader(headers, "cf-aig-authorization")
+ ) {
+ return;
+ }
+ throw new Error(`No API key for provider: ${provider}`);
+}
+
+interface ServerSentEvent {
+ event: string | null;
+ data: string;
+ raw: string[];
+}
+
+interface SseDecoderState {
+ event: string | null;
+ data: string[];
+ raw: string[];
+}
+
+const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet = new Set([
+ "message_start",
+ "message_delta",
+ "message_stop",
+ "content_block_start",
+ "content_block_delta",
+ "content_block_stop",
+]);
+
+function flushSseEvent(state: SseDecoderState): ServerSentEvent | null {
+ if (!state.event && state.data.length === 0) {
+ return null;
+ }
+
+ const event: ServerSentEvent = {
+ event: state.event,
+ data: state.data.join("\n"),
+ raw: [...state.raw],
+ };
+ state.event = null;
+ state.data = [];
+ state.raw = [];
+ return event;
+}
+
+function decodeSseLine(line: string, state: SseDecoderState): ServerSentEvent | null {
+ if (line === "") {
+ return flushSseEvent(state);
+ }
+
+ state.raw.push(line);
+ if (line.startsWith(":")) {
+ return null;
+ }
+
+ const delimiterIndex = line.indexOf(":");
+ const fieldName = delimiterIndex === -1 ? line : line.slice(0, delimiterIndex);
+ let value = delimiterIndex === -1 ? "" : line.slice(delimiterIndex + 1);
+ if (value.startsWith(" ")) {
+ value = value.slice(1);
+ }
+
+ if (fieldName === "event") {
+ state.event = value;
+ } else if (fieldName === "data") {
+ state.data.push(value);
+ }
+
+ return null;
+}
+
+function nextLineBreakIndex(text: string): number {
+ const carriageReturnIndex = text.indexOf("\r");
+ const newlineIndex = text.indexOf("\n");
+ if (carriageReturnIndex === -1) {
+ return newlineIndex;
+ }
+ if (newlineIndex === -1) {
+ return carriageReturnIndex;
+ }
+ return Math.min(carriageReturnIndex, newlineIndex);
+}
+
+function consumeLine(text: string): { line: string; rest: string } | null {
+ const lineBreakIndex = nextLineBreakIndex(text);
+ if (lineBreakIndex === -1) {
+ return null;
+ }
+
+ let nextIndex = lineBreakIndex + 1;
+ if (text[lineBreakIndex] === "\r" && text[nextIndex] === "\n") {
+ nextIndex += 1;
+ }
+
+ return {
+ line: text.slice(0, lineBreakIndex),
+ rest: text.slice(nextIndex),
+ };
+}
+
+async function* iterateSseMessages(
+ body: ReadableStream,
+ signal?: AbortSignal,
+): AsyncGenerator {
+ const reader = body.getReader();
+ const decoder = new TextDecoder();
+ const state: SseDecoderState = { event: null, data: [], raw: [] };
+ let buffer = "";
+
+ try {
+ while (true) {
+ if (signal?.aborted) {
+ throw new Error("Request was aborted");
+ }
+
+ const { value, done } = await reader.read();
+ if (done) {
+ break;
+ }
+
+ buffer += decoder.decode(value, { stream: true });
+ let consumed = consumeLine(buffer);
+ while (consumed) {
+ buffer = consumed.rest;
+ const event = decodeSseLine(consumed.line, state);
+ if (event) {
+ yield event;
+ }
+ consumed = consumeLine(buffer);
+ }
+ }
+
+ buffer += decoder.decode();
+ let consumed = consumeLine(buffer);
+ while (consumed) {
+ buffer = consumed.rest;
+ const event = decodeSseLine(consumed.line, state);
+ if (event) {
+ yield event;
+ }
+ consumed = consumeLine(buffer);
+ }
+
+ if (buffer.length > 0) {
+ const event = decodeSseLine(buffer, state);
+ if (event) {
+ yield event;
+ }
+ }
+
+ const trailingEvent = flushSseEvent(state);
+ if (trailingEvent) {
+ yield trailingEvent;
+ }
+ } finally {
+ reader.releaseLock();
+ }
+}
+
+async function* iterateAnthropicEvents(
+ response: Response,
+ signal?: AbortSignal,
+): AsyncGenerator {
+ if (!response.body) {
+ throw new Error("Attempted to iterate over an Anthropic response with no body");
+ }
+
+ let sawMessageStart = false;
+ let sawMessageEnd = false;
+
+ for await (const sse of iterateSseMessages(response.body, signal)) {
+ if (sse.event === "error") {
+ throw new Error(sse.data);
+ }
+
+ if (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? "")) {
+ continue;
+ }
+
+ try {
+ const event = parseJsonWithRepair(sse.data);
+ if (event.type === "message_start") {
+ sawMessageStart = true;
+ } else if (event.type === "message_stop") {
+ sawMessageEnd = true;
+ }
+ yield event;
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ throw new Error(
+ `Could not parse Anthropic SSE event ${sse.event}: ${message}; data=${sse.data}; raw=${sse.raw.join("\\n")}`,
+ );
+ }
+ }
+
+ if (sawMessageStart && !sawMessageEnd) {
+ throw new Error("Anthropic stream ended before message_stop");
+ }
+}
+
+export const stream: StreamFunction<"anthropic-messages", AnthropicOptions> = (
+ model: Model<"anthropic-messages">,
+ context: Context,
+ options?: AnthropicOptions,
+): AssistantMessageEventStream => {
+ const stream = new AssistantMessageEventStream();
+
+ (async () => {
+ const output: AssistantMessage = {
+ role: "assistant",
+ content: [],
+ api: model.api as 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: "stop",
+ timestamp: Date.now(),
+ };
+
+ try {
+ let client: Anthropic;
+ let isOAuth: boolean;
+
+ if (options?.client) {
+ client = options.client;
+ isOAuth = false;
+ } else {
+ const apiKey = options?.apiKey;
+ assertRequestAuth(model.provider, apiKey, options?.headers);
+
+ let copilotDynamicHeaders: Record | undefined;
+ if (model.provider === "github-copilot") {
+ const hasImages = hasCopilotVisionInput(context.messages);
+ copilotDynamicHeaders = buildCopilotDynamicHeaders({
+ messages: context.messages,
+ hasImages,
+ });
+ }
+
+ const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
+ const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
+
+ const created = createClient(
+ model,
+ apiKey,
+ options?.interleavedThinking ?? true,
+ shouldUseFineGrainedToolStreamingBeta(model, context),
+ options?.headers,
+ copilotDynamicHeaders,
+ cacheSessionId,
+ );
+ client = created.client;
+ isOAuth = created.isOAuthToken;
+ }
+ let params = buildParams(model, context, isOAuth, options);
+ const nextParams = await options?.onPayload?.(params, model);
+ if (nextParams !== undefined) {
+ params = nextParams as MessageCreateParamsStreaming;
+ }
+ const requestOptions = {
+ ...(options?.signal ? { signal: options.signal } : {}),
+ ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
+ maxRetries: options?.maxRetries ?? 0,
+ };
+ const response = await client.messages.create({ ...params, stream: true }, requestOptions).asResponse();
+ await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
+ stream.push({ type: "start", partial: output });
+
+ type Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & { index: number };
+ const blocks = output.content as Block[];
+
+ for await (const event of iterateAnthropicEvents(response, options?.signal)) {
+ if (event.type === "message_start") {
+ output.responseId = event.message.id;
+ // Capture initial token usage from message_start event
+ // This ensures we have input token counts even if the stream is aborted early
+ output.usage.input = event.message.usage.input_tokens || 0;
+ output.usage.output = event.message.usage.output_tokens || 0;
+ output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;
+ output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;
+ output.usage.cacheWrite1h = event.message.usage.cache_creation?.ephemeral_1h_input_tokens || 0;
+ // Anthropic doesn't provide total_tokens, compute from components
+ output.usage.totalTokens =
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
+ calculateCost(model, output.usage);
+ } else if (event.type === "content_block_start") {
+ if (event.content_block.type === "text") {
+ const block: Block = {
+ type: "text",
+ text: "",
+ index: event.index,
+ };
+ output.content.push(block);
+ stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
+ } else if (event.content_block.type === "thinking") {
+ const block: Block = {
+ type: "thinking",
+ thinking: "",
+ thinkingSignature: "",
+ index: event.index,
+ };
+ output.content.push(block);
+ stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
+ } else if (event.content_block.type === "redacted_thinking") {
+ const block: Block = {
+ type: "thinking",
+ thinking: "[Reasoning redacted]",
+ thinkingSignature: event.content_block.data,
+ redacted: true,
+ index: event.index,
+ };
+ output.content.push(block);
+ stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
+ } else if (event.content_block.type === "tool_use") {
+ const block: Block = {
+ type: "toolCall",
+ id: event.content_block.id,
+ name: isOAuth
+ ? fromClaudeCodeName(event.content_block.name, context.tools)
+ : event.content_block.name,
+ arguments: (event.content_block.input as Record) ?? {},
+ partialJson: "",
+ index: event.index,
+ };
+ output.content.push(block);
+ stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
+ }
+ } else if (event.type === "content_block_delta") {
+ if (event.delta.type === "text_delta") {
+ const index = blocks.findIndex((b) => b.index === event.index);
+ const block = blocks[index];
+ if (block && block.type === "text") {
+ block.text += event.delta.text;
+ stream.push({
+ type: "text_delta",
+ contentIndex: index,
+ delta: event.delta.text,
+ partial: output,
+ });
+ }
+ } else if (event.delta.type === "thinking_delta") {
+ const index = blocks.findIndex((b) => b.index === event.index);
+ const block = blocks[index];
+ if (block && block.type === "thinking") {
+ block.thinking += event.delta.thinking;
+ stream.push({
+ type: "thinking_delta",
+ contentIndex: index,
+ delta: event.delta.thinking,
+ partial: output,
+ });
+ }
+ } else if (event.delta.type === "input_json_delta") {
+ const index = blocks.findIndex((b) => b.index === event.index);
+ const block = blocks[index];
+ if (block && block.type === "toolCall") {
+ block.partialJson += event.delta.partial_json;
+ block.arguments = parseStreamingJson(block.partialJson);
+ stream.push({
+ type: "toolcall_delta",
+ contentIndex: index,
+ delta: event.delta.partial_json,
+ partial: output,
+ });
+ }
+ } else if (event.delta.type === "signature_delta") {
+ const index = blocks.findIndex((b) => b.index === event.index);
+ const block = blocks[index];
+ if (block && block.type === "thinking") {
+ block.thinkingSignature = block.thinkingSignature || "";
+ block.thinkingSignature += event.delta.signature;
+ }
+ }
+ } else if (event.type === "content_block_stop") {
+ const index = blocks.findIndex((b) => b.index === event.index);
+ const block = blocks[index];
+ if (block) {
+ delete (block as any).index;
+ if (block.type === "text") {
+ stream.push({
+ type: "text_end",
+ contentIndex: index,
+ content: block.text,
+ partial: output,
+ });
+ } else if (block.type === "thinking") {
+ stream.push({
+ type: "thinking_end",
+ contentIndex: index,
+ content: block.thinking,
+ partial: output,
+ });
+ } else if (block.type === "toolCall") {
+ block.arguments = parseStreamingJson(block.partialJson);
+ // Finalize in-place and strip the scratch buffer so replay only
+ // carries parsed arguments.
+ delete (block as { partialJson?: string }).partialJson;
+ stream.push({
+ type: "toolcall_end",
+ contentIndex: index,
+ toolCall: block,
+ partial: output,
+ });
+ }
+ }
+ } else if (event.type === "message_delta") {
+ if (event.delta.stop_reason) {
+ const stopReasonResult = mapStopReason(event.delta.stop_reason, event.delta.stop_details);
+ output.stopReason = stopReasonResult.stopReason;
+ if (stopReasonResult.errorMessage) {
+ output.errorMessage = stopReasonResult.errorMessage;
+ }
+ }
+ // Only update usage fields if present (not null).
+ // Preserves input_tokens from message_start when proxies omit it in message_delta.
+ if (event.usage.input_tokens != null) {
+ output.usage.input = event.usage.input_tokens;
+ }
+ if (event.usage.output_tokens != null) {
+ output.usage.output = event.usage.output_tokens;
+ }
+ if (event.usage.cache_read_input_tokens != null) {
+ output.usage.cacheRead = event.usage.cache_read_input_tokens;
+ }
+ if (event.usage.cache_creation_input_tokens != null) {
+ output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
+ }
+ // Anthropic doesn't provide total_tokens, compute from components
+ output.usage.totalTokens =
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
+ calculateCost(model, output.usage);
+ }
+ }
+
+ if (options?.signal?.aborted) {
+ throw new Error("Request was aborted");
+ }
+
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
+ throw new Error(output.errorMessage || "An unknown error occurred");
+ }
+
+ stream.push({ type: "done", reason: output.stopReason, message: output });
+ stream.end();
+ } catch (error) {
+ for (const block of output.content) {
+ delete (block as { index?: number }).index;
+ // partialJson is only a streaming scratch buffer; never persist it.
+ delete (block as { partialJson?: string }).partialJson;
+ }
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
+ output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
+ stream.push({ type: "error", reason: output.stopReason, error: output });
+ stream.end();
+ }
+ })();
+
+ return stream;
+};
+
+/**
+ * Map ThinkingLevel to Anthropic effort levels for adaptive thinking.
+ * Note: effort "max" is only valid on Opus 4.6, while Opus 4.7+ and Fable 5 support "xhigh".
+ */
+function mapThinkingLevelToEffort(
+ model: Model<"anthropic-messages">,
+ level: SimpleStreamOptions["reasoning"],
+): AnthropicEffort {
+ const mapped = level ? model.thinkingLevelMap?.[level] : undefined;
+ if (typeof mapped === "string") return mapped as AnthropicEffort;
+
+ switch (level) {
+ case "minimal":
+ case "low":
+ return "low";
+ case "medium":
+ return "medium";
+ case "high":
+ return "high";
+ default:
+ return "high";
+ }
+}
+
+export const streamSimple: StreamFunction<"anthropic-messages", SimpleStreamOptions> = (
+ model: Model<"anthropic-messages">,
+ context: Context,
+ options?: SimpleStreamOptions,
+): AssistantMessageEventStream => {
+ assertRequestAuth(model.provider, options?.apiKey, options?.headers);
+
+ const base = buildBaseOptions(model, options, options?.apiKey);
+ if (!options?.reasoning) {
+ return stream(model, context, { ...base, thinkingEnabled: false } satisfies AnthropicOptions);
+ }
+
+ // For models with adaptive thinking: use an effort level.
+ // For older models: use budget-based thinking.
+ if (model.compat?.forceAdaptiveThinking === true) {
+ const effort = mapThinkingLevelToEffort(model, options.reasoning);
+ return stream(model, context, {
+ ...base,
+ thinkingEnabled: true,
+ effort,
+ } satisfies AnthropicOptions);
+ }
+
+ // Undefined means the caller did not request an output cap; let the helper use the model cap.
+ // Do not coerce to 0 here, or the thinking budget would become the entire max_tokens value.
+ const adjusted = adjustMaxTokensForThinking(
+ base.maxTokens,
+ model.maxTokens,
+ options.reasoning,
+ options.thinkingBudgets,
+ );
+
+ return stream(model, context, {
+ ...base,
+ maxTokens: adjusted.maxTokens,
+ thinkingEnabled: true,
+ thinkingBudgetTokens: adjusted.thinkingBudget,
+ } satisfies AnthropicOptions);
+};
+
+function isOAuthToken(apiKey: string): boolean {
+ return apiKey.includes("sk-ant-oat");
+}
+
+function createClient(
+ model: Model<"anthropic-messages">,
+ apiKey: string | undefined,
+ interleavedThinking: boolean,
+ useFineGrainedToolStreamingBeta: boolean,
+ optionsHeaders?: ProviderHeaders,
+ dynamicHeaders?: Record,
+ sessionId?: string,
+): { client: Anthropic; isOAuthToken: boolean } {
+ // Adaptive thinking models have interleaved thinking built in, so skip the beta header.
+ const needsInterleavedBeta = interleavedThinking && model.compat?.forceAdaptiveThinking !== true;
+ const betaFeatures: string[] = [];
+ if (useFineGrainedToolStreamingBeta) {
+ betaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA);
+ }
+ if (needsInterleavedBeta) {
+ betaFeatures.push(INTERLEAVED_THINKING_BETA);
+ }
+
+ // Copilot: Bearer auth, selective betas.
+ if (model.provider === "github-copilot") {
+ const client = new Anthropic({
+ apiKey: null,
+ authToken: apiKey ?? null,
+ baseURL: model.baseUrl,
+ dangerouslyAllowBrowser: true,
+ defaultHeaders: mergeHeaders(
+ {
+ accept: "application/json",
+ "anthropic-dangerous-direct-browser-access": "true",
+ ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
+ },
+ model.headers,
+ dynamicHeaders,
+ optionsHeaders,
+ ),
+ });
+
+ return { client, isOAuthToken: false };
+ }
+
+ // OAuth: Bearer auth, Claude Code identity headers
+ if (apiKey && isOAuthToken(apiKey)) {
+ const client = new Anthropic({
+ apiKey: null,
+ authToken: apiKey,
+ baseURL: model.baseUrl,
+ dangerouslyAllowBrowser: true,
+ defaultHeaders: mergeHeaders(
+ {
+ accept: "application/json",
+ "anthropic-dangerous-direct-browser-access": "true",
+ "anthropic-beta": ["claude-code-20250219", "oauth-2025-04-20", ...betaFeatures].join(","),
+ "user-agent": `claude-cli/${claudeCodeVersion}`,
+ "x-app": "cli",
+ },
+ model.headers,
+ optionsHeaders,
+ ),
+ });
+
+ return { client, isOAuthToken: true };
+ }
+
+ // API key or header-owned auth.
+ const sessionAffinityHeaders: ProviderHeaders =
+ sessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { "x-session-affinity": sessionId } : {};
+ const defaultHeaders = mergeHeaders(
+ {
+ accept: "application/json",
+ "anthropic-dangerous-direct-browser-access": "true",
+ ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
+ },
+ sessionAffinityHeaders,
+ model.headers,
+ optionsHeaders,
+ );
+ const client = new Anthropic({
+ apiKey: apiKey ?? null,
+ authToken: null,
+ baseURL: model.baseUrl,
+ dangerouslyAllowBrowser: true,
+ defaultHeaders,
+ });
+
+ return { client, isOAuthToken: false };
+}
+
+function buildParams(
+ model: Model<"anthropic-messages">,
+ context: Context,
+ isOAuthToken: boolean,
+ options?: AnthropicOptions,
+): MessageCreateParamsStreaming {
+ const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);
+ const compat = getAnthropicCompat(model);
+ const params: MessageCreateParamsStreaming = {
+ model: model.id,
+ messages: convertMessages(context.messages, model, isOAuthToken, cacheControl, compat.allowEmptySignature),
+ max_tokens: options?.maxTokens ?? model.maxTokens,
+ stream: true,
+ };
+
+ // For OAuth tokens, we MUST include Claude Code identity
+ if (isOAuthToken) {
+ params.system = [
+ {
+ type: "text",
+ text: "You are Claude Code, Anthropic's official CLI for Claude.",
+ ...(cacheControl ? { cache_control: cacheControl } : {}),
+ },
+ ];
+ if (context.systemPrompt) {
+ params.system.push({
+ type: "text",
+ text: sanitizeSurrogates(context.systemPrompt),
+ ...(cacheControl ? { cache_control: cacheControl } : {}),
+ });
+ }
+ } else if (context.systemPrompt) {
+ // Add cache control to system prompt for non-OAuth tokens
+ params.system = [
+ {
+ type: "text",
+ text: sanitizeSurrogates(context.systemPrompt),
+ ...(cacheControl ? { cache_control: cacheControl } : {}),
+ },
+ ];
+ }
+
+ // Temperature is incompatible with extended thinking and unsupported on Claude Opus 4.7+.
+ if (options?.temperature !== undefined && !options?.thinkingEnabled && compat.supportsTemperature) {
+ params.temperature = options.temperature;
+ }
+
+ if (context.tools && context.tools.length > 0) {
+ params.tools = convertTools(
+ context.tools,
+ isOAuthToken,
+ compat.supportsEagerToolInputStreaming,
+ compat.supportsCacheControlOnTools ? cacheControl : undefined,
+ );
+ }
+
+ // Configure thinking mode: adaptive, budget-based, or explicitly disabled.
+ if (model.reasoning) {
+ if (options?.thinkingEnabled) {
+ // Default to "summarized" so Opus 4.7 and Mythos Preview behave like
+ // older Claude 4 models (whose API default is also "summarized").
+ const display: AnthropicThinkingDisplay = options.thinkingDisplay ?? "summarized";
+ if (model.compat?.forceAdaptiveThinking === true) {
+ // Adaptive thinking: Claude decides when and how much to think.
+ params.thinking = { type: "adaptive", display };
+ if (options.effort) {
+ // The Anthropic SDK types can lag newly supported effort values such as "xhigh".
+ params.output_config =
+ options.effort === "xhigh"
+ ? ({ effort: options.effort } as unknown as NonNullable<
+ MessageCreateParamsStreaming["output_config"]
+ >)
+ : { effort: options.effort };
+ }
+ } else {
+ // Budget-based thinking for older models
+ params.thinking = {
+ type: "enabled",
+ budget_tokens: options.thinkingBudgetTokens || 1024,
+ display,
+ };
+ }
+ } else if (options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null) {
+ params.thinking = { type: "disabled" };
+ }
+ }
+
+ if (options?.metadata) {
+ const userId = options.metadata.user_id;
+ if (typeof userId === "string") {
+ params.metadata = { user_id: userId };
+ }
+ }
+
+ if (options?.toolChoice) {
+ if (typeof options.toolChoice === "string") {
+ params.tool_choice = { type: options.toolChoice };
+ } else {
+ params.tool_choice = options.toolChoice;
+ }
+ }
+
+ return params;
+}
+
+// Normalize tool call IDs to match Anthropic's required pattern and length
+function normalizeToolCallId(id: string): string {
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
+}
+
+function convertMessages(
+ messages: Message[],
+ model: Model<"anthropic-messages">,
+ isOAuthToken: boolean,
+ cacheControl?: CacheControlEphemeral,
+ allowEmptySignature = false,
+): MessageParam[] {
+ const params: MessageParam[] = [];
+
+ // Transform messages for cross-provider compatibility
+ const transformedMessages = transformMessages(messages, model, normalizeToolCallId);
+
+ for (let i = 0; i < transformedMessages.length; i++) {
+ const msg = transformedMessages[i];
+
+ if (msg.role === "user") {
+ if (typeof msg.content === "string") {
+ if (msg.content.trim().length > 0) {
+ params.push({
+ role: "user",
+ content: sanitizeSurrogates(msg.content),
+ });
+ }
+ } else {
+ const blocks: ContentBlockParam[] = msg.content.map((item) => {
+ if (item.type === "text") {
+ return {
+ type: "text",
+ text: sanitizeSurrogates(item.text),
+ };
+ } else {
+ return {
+ type: "image",
+ source: {
+ type: "base64",
+ media_type: item.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
+ data: item.data,
+ },
+ };
+ }
+ });
+ const filteredBlocks = blocks.filter((b) => {
+ if (b.type === "text") {
+ return b.text.trim().length > 0;
+ }
+ return true;
+ });
+ if (filteredBlocks.length === 0) continue;
+ params.push({
+ role: "user",
+ content: filteredBlocks,
+ });
+ }
+ } else if (msg.role === "assistant") {
+ const blocks: ContentBlockParam[] = [];
+
+ for (const block of msg.content) {
+ if (block.type === "text") {
+ if (block.text.trim().length === 0) continue;
+ blocks.push({
+ type: "text",
+ text: sanitizeSurrogates(block.text),
+ });
+ } else if (block.type === "thinking") {
+ // Redacted thinking: pass the opaque payload back as redacted_thinking
+ if (block.redacted) {
+ blocks.push({
+ type: "redacted_thinking",
+ data: block.thinkingSignature!,
+ });
+ continue;
+ }
+ if (block.thinking.trim().length === 0) continue;
+ // If thinking signature is missing/empty (e.g., from aborted stream),
+ // convert to plain text for Anthropic. Some compatible providers emit
+ // and accept empty signatures, so let marked models preserve the block.
+ if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
+ blocks.push(
+ allowEmptySignature
+ ? {
+ type: "thinking",
+ thinking: sanitizeSurrogates(block.thinking),
+ signature: "",
+ }
+ : {
+ type: "text",
+ text: sanitizeSurrogates(block.thinking),
+ },
+ );
+ } else {
+ blocks.push({
+ type: "thinking",
+ thinking: sanitizeSurrogates(block.thinking),
+ signature: block.thinkingSignature,
+ });
+ }
+ } else if (block.type === "toolCall") {
+ blocks.push({
+ type: "tool_use",
+ id: block.id,
+ name: isOAuthToken ? toClaudeCodeName(block.name) : block.name,
+ input: block.arguments ?? {},
+ });
+ }
+ }
+ if (blocks.length === 0) continue;
+ params.push({
+ role: "assistant",
+ content: blocks,
+ });
+ } else if (msg.role === "toolResult") {
+ // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint
+ const toolResults: ContentBlockParam[] = [];
+
+ // Add the current tool result
+ toolResults.push({
+ type: "tool_result",
+ tool_use_id: msg.toolCallId,
+ content: convertContentBlocks(msg.content),
+ is_error: msg.isError,
+ });
+
+ // Look ahead for consecutive toolResult messages
+ let j = i + 1;
+ while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
+ const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult
+ toolResults.push({
+ type: "tool_result",
+ tool_use_id: nextMsg.toolCallId,
+ content: convertContentBlocks(nextMsg.content),
+ is_error: nextMsg.isError,
+ });
+ j++;
+ }
+
+ // Skip the messages we've already processed
+ i = j - 1;
+
+ // Add a single user message with all tool results
+ params.push({
+ role: "user",
+ content: toolResults,
+ });
+ }
+ }
+
+ // Add cache_control to the last user message to cache conversation history
+ if (cacheControl && params.length > 0) {
+ const lastMessage = params[params.length - 1];
+ if (lastMessage.role === "user") {
+ if (Array.isArray(lastMessage.content)) {
+ const lastBlock = lastMessage.content[lastMessage.content.length - 1];
+ if (
+ lastBlock &&
+ (lastBlock.type === "text" || lastBlock.type === "image" || lastBlock.type === "tool_result")
+ ) {
+ (lastBlock as any).cache_control = cacheControl;
+ }
+ } else if (typeof lastMessage.content === "string") {
+ lastMessage.content = [
+ {
+ type: "text",
+ text: lastMessage.content,
+ cache_control: cacheControl,
+ },
+ ] as any;
+ }
+ }
+ }
+
+ return params;
+}
+
+function shouldUseFineGrainedToolStreamingBeta(model: Model<"anthropic-messages">, context: Context): boolean {
+ return !!context.tools?.length && !getAnthropicCompat(model).supportsEagerToolInputStreaming;
+}
+
+function convertTools(
+ tools: Tool[],
+ isOAuthToken: boolean,
+ supportsEagerToolInputStreaming: boolean,
+ cacheControl?: CacheControlEphemeral,
+): Anthropic.Messages.Tool[] {
+ if (!tools) return [];
+
+ return tools.map((tool, index) => {
+ const schema = tool.parameters as { properties?: unknown; required?: string[] };
+
+ return {
+ name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name,
+ description: tool.description,
+ ...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),
+ input_schema: {
+ type: "object",
+ properties: schema.properties ?? {},
+ required: schema.required ?? [],
+ },
+ ...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}),
+ };
+ });
+}
+
+function mapStopReason(
+ reason: Anthropic.Messages.StopReason | string,
+ stopDetails?: RefusalStopDetails | null,
+): { stopReason: StopReason; errorMessage?: string } {
+ switch (reason) {
+ case "end_turn":
+ return { stopReason: "stop" };
+ case "max_tokens":
+ return { stopReason: "length" };
+ case "tool_use":
+ return { stopReason: "toolUse" };
+ case "refusal":
+ return {
+ stopReason: "error",
+ errorMessage: stopDetails?.explanation || `The model refused to complete the request`,
+ };
+ case "pause_turn": // Stop is good enough -> resubmit
+ return { stopReason: "stop" };
+ case "stop_sequence":
+ return { stopReason: "stop" }; // We don't supply stop sequences, so this should never happen
+ case "sensitive": // Content flagged by safety filters (not yet in SDK types)
+ return { stopReason: "error" };
+ default:
+ // Handle unknown stop reasons gracefully (API may add new values)
+ throw new Error(`Unhandled stop reason: ${reason}`);
+ }
+}
diff --git a/packages/ai/src/api/azure-openai-responses.lazy.ts b/packages/ai/src/api/azure-openai-responses.lazy.ts
new file mode 100644
index 00000000..5921e10f
--- /dev/null
+++ b/packages/ai/src/api/azure-openai-responses.lazy.ts
@@ -0,0 +1,4 @@
+import type { ProviderStreams } from "../types.ts";
+import { lazyApi } from "./lazy.ts";
+
+export const azureOpenAIResponsesApi = (): ProviderStreams => lazyApi(() => import("./azure-openai-responses.ts"));
diff --git a/packages/ai/src/api/azure-openai-responses.ts b/packages/ai/src/api/azure-openai-responses.ts
new file mode 100644
index 00000000..137c43c9
--- /dev/null
+++ b/packages/ai/src/api/azure-openai-responses.ts
@@ -0,0 +1,307 @@
+import { AzureOpenAI } from "openai";
+import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
+import { clampThinkingLevel } from "../models.ts";
+import type {
+ Api,
+ AssistantMessage,
+ Context,
+ Model,
+ SimpleStreamOptions,
+ StreamFunction,
+ StreamOptions,
+} from "../types.ts";
+import { AssistantMessageEventStream } from "../utils/event-stream.ts";
+import { headersToRecord } from "../utils/headers.ts";
+import { getProviderEnvValue } from "../utils/provider-env.ts";
+import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
+import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts";
+import { buildBaseOptions } from "./simple-options.ts";
+
+const DEFAULT_AZURE_API_VERSION = "v1";
+const AZURE_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode", "azure-openai-responses"]);
+
+function parseDeploymentNameMap(value: string | undefined): Map {
+ const map = new Map();
+ if (!value) return map;
+ for (const entry of value.split(",")) {
+ const trimmed = entry.trim();
+ if (!trimmed) continue;
+ const [modelId, deploymentName] = trimmed.split("=", 2);
+ if (!modelId || !deploymentName) continue;
+ map.set(modelId.trim(), deploymentName.trim());
+ }
+ return map;
+}
+
+function resolveDeploymentName(model: Model<"azure-openai-responses">, options?: AzureOpenAIResponsesOptions): string {
+ if (options?.azureDeploymentName) {
+ return options.azureDeploymentName;
+ }
+ const mappedDeployment = parseDeploymentNameMap(
+ getProviderEnvValue("AZURE_OPENAI_DEPLOYMENT_NAME_MAP", options?.env),
+ ).get(model.id);
+ return mappedDeployment || model.id;
+}
+
+function formatAzureOpenAIError(error: unknown): string {
+ if (error instanceof Error) {
+ const status = (error as Error & { status?: unknown }).status;
+ const statusCode = typeof status === "number" ? status : undefined;
+ if (statusCode !== undefined) {
+ return `Azure OpenAI API error (${statusCode}): ${error.message}`;
+ }
+ return error.message;
+ }
+ try {
+ return JSON.stringify(error);
+ } catch {
+ return String(error);
+ }
+}
+
+// Azure OpenAI Responses-specific options
+export interface AzureOpenAIResponsesOptions extends StreamOptions {
+ reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
+ reasoningSummary?: "auto" | "detailed" | "concise" | null;
+ azureApiVersion?: string;
+ azureResourceName?: string;
+ azureBaseUrl?: string;
+ azureDeploymentName?: string;
+}
+
+/**
+ * Generate function for Azure OpenAI Responses API
+ */
+export const stream: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions> = (
+ model: Model<"azure-openai-responses">,
+ context: Context,
+ options?: AzureOpenAIResponsesOptions,
+): AssistantMessageEventStream => {
+ const stream = new AssistantMessageEventStream();
+
+ // Start async processing
+ (async () => {
+ const deploymentName = resolveDeploymentName(model, options);
+
+ const output: AssistantMessage = {
+ role: "assistant",
+ content: [],
+ api: "azure-openai-responses" as 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: "stop",
+ timestamp: Date.now(),
+ };
+
+ try {
+ // Create Azure OpenAI client
+ const apiKey = options?.apiKey;
+ if (!apiKey) {
+ throw new Error(`No API key for provider: ${model.provider}`);
+ }
+ const client = createClient(model, apiKey, options);
+ let params = buildParams(model, context, options, deploymentName);
+ const nextParams = await options?.onPayload?.(params, model);
+ if (nextParams !== undefined) {
+ params = nextParams as ResponseCreateParamsStreaming;
+ }
+ const requestOptions = {
+ ...(options?.signal ? { signal: options.signal } : {}),
+ ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
+ maxRetries: options?.maxRetries ?? 0,
+ };
+ const { data: openaiStream, response } = await client.responses.create(params, requestOptions).withResponse();
+ await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
+ stream.push({ type: "start", partial: output });
+
+ await processResponsesStream(openaiStream, output, stream, model);
+
+ if (options?.signal?.aborted) {
+ throw new Error("Request was aborted");
+ }
+
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
+ throw new Error("An unknown error occurred");
+ }
+
+ stream.push({ type: "done", reason: output.stopReason, message: output });
+ stream.end();
+ } catch (error) {
+ for (const block of output.content) {
+ delete (block as { index?: number }).index;
+ // partialJson is only a streaming scratch buffer; never persist it.
+ delete (block as { partialJson?: string }).partialJson;
+ }
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
+ output.errorMessage = formatAzureOpenAIError(error);
+ stream.push({ type: "error", reason: output.stopReason, error: output });
+ stream.end();
+ }
+ })();
+
+ return stream;
+};
+
+export const streamSimple: StreamFunction<"azure-openai-responses", SimpleStreamOptions> = (
+ model: Model<"azure-openai-responses">,
+ context: Context,
+ options?: SimpleStreamOptions,
+): AssistantMessageEventStream => {
+ const apiKey = options?.apiKey;
+ if (!apiKey) {
+ throw new Error(`No API key for provider: ${model.provider}`);
+ }
+
+ const base = buildBaseOptions(model, options, apiKey);
+ const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
+ const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
+
+ return stream(model, context, {
+ ...base,
+ reasoningEffort,
+ } satisfies AzureOpenAIResponsesOptions);
+};
+
+function normalizeAzureBaseUrl(baseUrl: string): string {
+ const trimmed = baseUrl.trim().replace(/\/+$/, "");
+ let url: URL;
+ try {
+ url = new URL(trimmed);
+ } catch {
+ throw new Error(`Invalid Azure OpenAI base URL: ${baseUrl}`);
+ }
+
+ const isAzureHost =
+ url.hostname.endsWith(".openai.azure.com") ||
+ url.hostname.endsWith(".cognitiveservices.azure.com") ||
+ url.hostname.endsWith(".ai.azure.com");
+ const normalizedPath = url.pathname.replace(/\/+$/, "");
+
+ // Ensure Azure hosts have /openai/v1 as base path so the AzureOpenAI SDK
+ // can append /deployments//... and ?api-version=v1 correctly.
+ if (
+ isAzureHost &&
+ (normalizedPath === "" ||
+ normalizedPath === "/" ||
+ normalizedPath === "/openai" ||
+ normalizedPath === "/openai/v1/responses")
+ ) {
+ url.pathname = "/openai/v1";
+ url.search = "";
+ }
+
+ return url.toString().replace(/\/+$/, "");
+}
+
+function buildDefaultBaseUrl(resourceName: string): string {
+ return `https://${resourceName}.openai.azure.com/openai/v1`;
+}
+
+function resolveAzureConfig(
+ model: Model<"azure-openai-responses">,
+ options?: AzureOpenAIResponsesOptions,
+): { baseUrl: string; apiVersion: string } {
+ const apiVersion =
+ options?.azureApiVersion ||
+ getProviderEnvValue("AZURE_OPENAI_API_VERSION", options?.env) ||
+ DEFAULT_AZURE_API_VERSION;
+
+ const baseUrl =
+ options?.azureBaseUrl?.trim() || getProviderEnvValue("AZURE_OPENAI_BASE_URL", options?.env)?.trim() || undefined;
+ const resourceName = options?.azureResourceName || getProviderEnvValue("AZURE_OPENAI_RESOURCE_NAME", options?.env);
+
+ let resolvedBaseUrl = baseUrl;
+
+ if (!resolvedBaseUrl && resourceName) {
+ resolvedBaseUrl = buildDefaultBaseUrl(resourceName);
+ }
+
+ if (!resolvedBaseUrl && model.baseUrl) {
+ resolvedBaseUrl = model.baseUrl;
+ }
+
+ if (!resolvedBaseUrl) {
+ throw new Error(
+ "Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, or pass azureBaseUrl, azureResourceName, or model.baseUrl.",
+ );
+ }
+
+ return {
+ baseUrl: normalizeAzureBaseUrl(resolvedBaseUrl),
+ apiVersion,
+ };
+}
+
+function createClient(model: Model<"azure-openai-responses">, apiKey: string, options?: AzureOpenAIResponsesOptions) {
+ const headers = { ...model.headers };
+
+ if (options?.headers) {
+ Object.assign(headers, options.headers);
+ }
+
+ const { baseUrl, apiVersion } = resolveAzureConfig(model, options);
+
+ return new AzureOpenAI({
+ apiKey,
+ apiVersion,
+ dangerouslyAllowBrowser: true,
+ defaultHeaders: headers,
+ baseURL: baseUrl,
+ });
+}
+
+function buildParams(
+ model: Model<"azure-openai-responses">,
+ context: Context,
+ options: AzureOpenAIResponsesOptions | undefined,
+ deploymentName: string,
+) {
+ const messages = convertResponsesMessages(model, context, AZURE_TOOL_CALL_PROVIDERS);
+
+ const params: ResponseCreateParamsStreaming = {
+ model: deploymentName,
+ input: messages,
+ stream: true,
+ prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId),
+ store: false,
+ };
+
+ if (options?.maxTokens) {
+ params.max_output_tokens = options?.maxTokens;
+ }
+
+ if (options?.temperature !== undefined) {
+ params.temperature = options?.temperature;
+ }
+
+ if (context.tools && context.tools.length > 0) {
+ params.tools = convertResponsesTools(context.tools);
+ }
+
+ if (model.reasoning) {
+ if (options?.reasoningEffort || options?.reasoningSummary) {
+ const effort = options?.reasoningEffort
+ ? (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort)
+ : "medium";
+ params.reasoning = {
+ effort: effort as NonNullable["effort"],
+ summary: options?.reasoningSummary || "auto",
+ };
+ params.include = ["reasoning.encrypted_content"];
+ } else if (model.thinkingLevelMap?.off !== null) {
+ params.reasoning = {
+ effort: (model.thinkingLevelMap?.off ?? "none") as NonNullable["effort"],
+ };
+ }
+ }
+
+ return params;
+}
diff --git a/packages/ai/src/api/bedrock-converse-stream.lazy.ts b/packages/ai/src/api/bedrock-converse-stream.lazy.ts
new file mode 100644
index 00000000..f9b30b35
--- /dev/null
+++ b/packages/ai/src/api/bedrock-converse-stream.lazy.ts
@@ -0,0 +1,30 @@
+import type { ProviderStreams } from "../types.ts";
+import { lazyApi } from "./lazy.ts";
+
+/**
+ * Loads the bedrock implementation through a variable specifier so bundlers
+ * (browser smoke, Bun compile) cannot follow the import into the Node-only
+ * AWS SDK. The `.ts`/`.js` rewrite keeps the trick working from both source
+ * and built output.
+ */
+const importNodeOnlyApi = (specifier: string): Promise => {
+ const runtimeSpecifier = import.meta.url.endsWith(".js") ? specifier.replace(/\.ts$/, ".js") : specifier;
+ return import(runtimeSpecifier);
+};
+
+let bedrockModuleOverride: ProviderStreams | undefined;
+
+/**
+ * Overrides the dynamically imported bedrock implementation. Used by the Bun
+ * binary build, where the variable-specifier import cannot be bundled; the
+ * build registers a statically imported module instead.
+ */
+export function setBedrockProviderModule(module: ProviderStreams): void {
+ bedrockModuleOverride = module;
+}
+
+export const bedrockConverseStreamApi = (): ProviderStreams =>
+ lazyApi(
+ async () =>
+ bedrockModuleOverride ?? ((await importNodeOnlyApi("./bedrock-converse-stream.ts")) as ProviderStreams),
+ );
diff --git a/packages/ai/src/api/bedrock-converse-stream.ts b/packages/ai/src/api/bedrock-converse-stream.ts
new file mode 100644
index 00000000..68af206d
--- /dev/null
+++ b/packages/ai/src/api/bedrock-converse-stream.ts
@@ -0,0 +1,1063 @@
+import type { Agent as HttpsAgent } from "node:https";
+import {
+ BedrockRuntimeClient,
+ type BedrockRuntimeClientConfig,
+ BedrockRuntimeServiceException,
+ StopReason as BedrockStopReason,
+ type Tool as BedrockTool,
+ CachePointType,
+ CacheTTL,
+ type ContentBlock,
+ type ContentBlockDeltaEvent,
+ type ContentBlockStartEvent,
+ type ContentBlockStopEvent,
+ ConversationRole,
+ ConverseStreamCommand,
+ type ConverseStreamMetadataEvent,
+ ImageFormat,
+ type Message,
+ type SystemContentBlock,
+ type ToolChoice,
+ type ToolConfiguration,
+ type ToolResultContentBlock,
+ ToolResultStatus,
+} from "@aws-sdk/client-bedrock-runtime";
+import { NodeHttpHandler } from "@smithy/node-http-handler";
+import type { BuildMiddleware, DocumentType, MetadataBearer } from "@smithy/types";
+import { HttpProxyAgent } from "http-proxy-agent";
+import { HttpsProxyAgent } from "https-proxy-agent";
+import { calculateCost } from "../models.ts";
+import type {
+ Api,
+ AssistantMessage,
+ CacheRetention,
+ Context,
+ ImageContent,
+ Model,
+ ProviderEnv,
+ SimpleStreamOptions,
+ StopReason,
+ StreamFunction,
+ StreamOptions,
+ TextContent,
+ ThinkingBudgets,
+ ThinkingContent,
+ ThinkingLevel,
+ Tool,
+ ToolCall,
+ ToolResultMessage,
+} from "../types.ts";
+import { AssistantMessageEventStream } from "../utils/event-stream.ts";
+import { providerHeadersToRecord } from "../utils/headers.ts";
+import { parseStreamingJson } from "../utils/json-parse.ts";
+import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.ts";
+import { getProviderEnvValue } from "../utils/provider-env.ts";
+import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
+import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.ts";
+import { transformMessages } from "./transform-messages.ts";
+
+export type BedrockThinkingDisplay = "summarized" | "omitted";
+
+export interface BedrockOptions extends StreamOptions {
+ region?: string;
+ profile?: string;
+ toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
+ /* See https://docs.aws.amazon.com/bedrock/latest/userguide/inference-reasoning.html for supported models. */
+ reasoning?: ThinkingLevel;
+ /* Custom token budgets per thinking level. Overrides default budgets. */
+ thinkingBudgets?: ThinkingBudgets;
+ /* Only supported by Claude 4.x models, see https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-extended-thinking.html#claude-messages-extended-thinking-tool-use-interleaved */
+ interleavedThinking?: boolean;
+ /**
+ * Controls how Claude's thinking content is returned in responses.
+ * - "summarized": Thinking blocks contain summarized thinking text (default here).
+ * - "omitted": Thinking content is redacted but the signature still travels back
+ * for multi-turn continuity, reducing time-to-first-text-token.
+ *
+ * Note: Anthropic's API default for Claude Opus 4.8 and Mythos Preview is
+ * "omitted". We default to "summarized" here to keep behavior consistent with
+ * older Claude 4 models. Only applies to Claude models on Bedrock.
+ */
+ thinkingDisplay?: BedrockThinkingDisplay;
+ /** Key-value pairs attached to the inference request for cost allocation tagging.
+ * Keys: max 64 chars, no `aws:` prefix. Values: max 256 chars. Max 50 pairs.
+ * Tags appear in AWS Cost Explorer split cost allocation data.
+ * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html */
+ requestMetadata?: Record;
+ /** Bearer token for Bedrock API key authentication.
+ * When set, bypasses SigV4 signing and sends Authorization: Bearer instead.
+ * Requires `bedrock:CallWithBearerToken` IAM permission on the token's identity.
+ * Set via AWS_BEARER_TOKEN_BEDROCK env var or pass directly.
+ * @see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrock.html */
+ bearerToken?: string;
+}
+
+type Block = (TextContent | ThinkingContent | ToolCall) & { index?: number; partialJson?: string };
+
+const EMPTY_TEXT_PLACEHOLDER = "";
+
+export const stream: StreamFunction<"bedrock-converse-stream", BedrockOptions> = (
+ model: Model<"bedrock-converse-stream">,
+ context: Context,
+ options: BedrockOptions = {},
+): AssistantMessageEventStream => {
+ const stream = new AssistantMessageEventStream();
+
+ (async () => {
+ const output: AssistantMessage = {
+ role: "assistant",
+ content: [],
+ api: "bedrock-converse-stream" as 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: "stop",
+ timestamp: Date.now(),
+ };
+
+ const blocks = output.content as Block[];
+
+ const config: BedrockRuntimeClientConfig = {
+ profile: options.profile || getProviderEnvValue("AWS_PROFILE", options.env),
+ };
+ const configuredRegion = getConfiguredBedrockRegion(options);
+ const hasAmbientConfiguredProfile = Boolean(getProviderEnvValue("AWS_PROFILE"));
+ const endpointRegion = getStandardBedrockEndpointRegion(model.baseUrl);
+ const useExplicitEndpoint = shouldUseExplicitBedrockEndpoint(
+ model.baseUrl,
+ configuredRegion,
+ hasAmbientConfiguredProfile,
+ );
+
+ // Only pin standard AWS Bedrock runtime endpoints when no region or ambient AWS_PROFILE is configured.
+ // This preserves custom endpoints (VPC/proxy) from #3402 without forcing built-in
+ // catalog defaults such as us-east-1 to override AWS_REGION/AWS_PROFILE.
+ if (useExplicitEndpoint) {
+ config.endpoint = model.baseUrl;
+ }
+
+ // Resolve bearer token for Bedrock API key auth.
+ const skipAuth = getProviderEnvValue("AWS_BEDROCK_SKIP_AUTH", options.env) === "1";
+ const bearerToken =
+ options.bearerToken || getProviderEnvValue("AWS_BEARER_TOKEN_BEDROCK", options.env) || undefined;
+ const useBearerToken = bearerToken !== undefined && !skipAuth;
+
+ // in Node.js/Bun environment only
+ if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
+ // Region resolution: ARN-embedded > explicit option > env vars > SDK default chain.
+ // When the model ID is an inference profile ARN, extract the region from it.
+ // This avoids conflicts with AWS_REGION set for other services.
+ const arnRegionMatch = model.id.match(/^arn:aws(?:-[a-z0-9-]+)?:bedrock:([a-z0-9-]+):/);
+ if (arnRegionMatch) {
+ config.region = arnRegionMatch[1];
+ } else if (configuredRegion) {
+ config.region = configuredRegion;
+ } else if (endpointRegion && useExplicitEndpoint) {
+ config.region = endpointRegion;
+ } else if (!hasAmbientConfiguredProfile) {
+ config.region = "us-east-1";
+ }
+
+ // Support proxies that don't need authentication
+ if (skipAuth) {
+ config.credentials = {
+ accessKeyId: "dummy-access-key",
+ secretAccessKey: "dummy-secret-key",
+ };
+ }
+
+ const credentials = getConfiguredBedrockCredentials(options.env);
+ if (!skipAuth && credentials) {
+ config.credentials = credentials;
+ }
+
+ const proxyUrl = resolveHttpProxyUrlForTarget(model.baseUrl, options.env);
+ if (proxyUrl) {
+ // Bedrock runtime uses NodeHttp2Handler by default since v3.798.0, which is based
+ // on `http2` module and has no support for http agent.
+ // Use NodeHttpHandler to support HTTP(S) proxy agents.
+ config.requestHandler = new NodeHttpHandler({
+ httpAgent: new HttpProxyAgent(proxyUrl),
+ httpsAgent: new HttpsProxyAgent(proxyUrl) as unknown as HttpsAgent,
+ });
+ } else if (getProviderEnvValue("AWS_BEDROCK_FORCE_HTTP1", options.env) === "1") {
+ // Some custom endpoints require HTTP/1.1 instead of HTTP/2
+ config.requestHandler = new NodeHttpHandler();
+ }
+ } else {
+ // Non-Node environment (browser): fall back to us-east-1 since
+ // there's no config file resolution available.
+ config.region =
+ configuredRegion || (endpointRegion && useExplicitEndpoint ? endpointRegion : undefined) || "us-east-1";
+ }
+
+ if (useBearerToken) {
+ config.token = { token: bearerToken };
+ config.authSchemePreference = ["httpBearerAuth"];
+ }
+
+ try {
+ const client = new BedrockRuntimeClient(config);
+ const customHeaders = providerHeadersToRecord(options.headers);
+ if (customHeaders) {
+ addCustomHeadersMiddleware(client, customHeaders);
+ }
+ const cacheRetention = resolveCacheRetention(options.cacheRetention, options.env);
+ const inferenceMaxTokens = options.maxTokens ?? (isAnthropicClaudeModel(model) ? model.maxTokens : undefined);
+ let commandInput = {
+ modelId: model.id,
+ messages: convertMessages(context, model, cacheRetention, options.env),
+ system: buildSystemPrompt(context.systemPrompt, model, cacheRetention, options.env),
+ inferenceConfig: {
+ ...(inferenceMaxTokens !== undefined && { maxTokens: inferenceMaxTokens }),
+ ...(options.temperature !== undefined && { temperature: options.temperature }),
+ },
+ toolConfig: convertToolConfig(context.tools, options.toolChoice),
+ additionalModelRequestFields: buildAdditionalModelRequestFields(model, options),
+ ...(options.requestMetadata !== undefined && { requestMetadata: options.requestMetadata }),
+ };
+ const nextCommandInput = await options?.onPayload?.(commandInput, model);
+ if (nextCommandInput !== undefined) {
+ commandInput = nextCommandInput as typeof commandInput;
+ }
+ const command = new ConverseStreamCommand(commandInput);
+
+ const response = await client.send(command, { abortSignal: options.signal });
+ if (response.$metadata.httpStatusCode !== undefined) {
+ const responseHeaders: Record = {};
+ if (response.$metadata.requestId) {
+ responseHeaders["x-amzn-requestid"] = response.$metadata.requestId;
+ }
+ await options?.onResponse?.({ status: response.$metadata.httpStatusCode, headers: responseHeaders }, model);
+ }
+
+ for await (const item of response.stream!) {
+ if (item.messageStart) {
+ if (item.messageStart.role !== ConversationRole.ASSISTANT) {
+ throw new Error("Unexpected assistant message start but got user message start instead");
+ }
+ stream.push({ type: "start", partial: output });
+ } else if (item.contentBlockStart) {
+ handleContentBlockStart(item.contentBlockStart, blocks, output, stream);
+ } else if (item.contentBlockDelta) {
+ handleContentBlockDelta(item.contentBlockDelta, blocks, output, stream);
+ } else if (item.contentBlockStop) {
+ handleContentBlockStop(item.contentBlockStop, blocks, output, stream);
+ } else if (item.messageStop) {
+ output.stopReason = mapStopReason(item.messageStop.stopReason);
+ } else if (item.metadata) {
+ handleMetadata(item.metadata, model, output);
+ } else if (item.internalServerException) {
+ throw item.internalServerException;
+ } else if (item.modelStreamErrorException) {
+ throw item.modelStreamErrorException;
+ } else if (item.validationException) {
+ throw item.validationException;
+ } else if (item.throttlingException) {
+ throw item.throttlingException;
+ } else if (item.serviceUnavailableException) {
+ throw item.serviceUnavailableException;
+ }
+ }
+
+ if (options.signal?.aborted) {
+ throw new Error("Request was aborted");
+ }
+
+ if (output.stopReason === "error" || output.stopReason === "aborted") {
+ throw new Error("An unknown error occurred");
+ }
+
+ stream.push({ type: "done", reason: output.stopReason, message: output });
+ stream.end();
+ } catch (error) {
+ for (const block of output.content) {
+ delete (block as Block).index;
+ // partialJson is only a streaming scratch buffer; never persist it.
+ delete (block as Block).partialJson;
+ }
+ output.stopReason = options.signal?.aborted ? "aborted" : "error";
+ output.errorMessage = formatBedrockError(error);
+ stream.push({ type: "error", reason: output.stopReason, error: output });
+ stream.end();
+ }
+ })();
+
+ return stream;
+};
+
+/**
+ * Human-readable prefixes for Bedrock SDK exception names.
+ * The downstream retry logic in agent-session matches patterns like
+ * `server.?error` and `service.?unavailable`, so we preserve the legacy
+ * prefix format rather than using the raw SDK exception name.
+ */
+const BEDROCK_ERROR_PREFIXES: Record = {
+ InternalServerException: "Internal server error",
+ ModelStreamErrorException: "Model stream error",
+ ValidationException: "Validation error",
+ ThrottlingException: "Throttling error",
+ ServiceUnavailableException: "Service unavailable",
+};
+
+/**
+ * Some models reject the account/profile's configured Bedrock data retention mode
+ * (e.g. "data retention mode 'default' is not available for this model"). Point
+ * users at the AWS docs explaining how to configure a supported mode.
+ */
+const BEDROCK_DATA_RETENTION_DOCS_URL = "https://docs.aws.amazon.com/bedrock/latest/userguide/data-retention.html";
+
+/**
+ * Format a Bedrock error with a human-readable prefix.
+ * AWS SDK exceptions (both from `client.send()` and from stream event items)
+ * extend BedrockRuntimeServiceException. We map the `.name` to a stable
+ * human-readable prefix so downstream consumers (retry logic, context-overflow
+ * detection) can distinguish error categories via simple string matching.
+ */
+function formatBedrockError(error: unknown): string {
+ const message = error instanceof Error ? error.message : JSON.stringify(error);
+ const dataRetentionHint = /data retention mode/i.test(message)
+ ? ` See ${BEDROCK_DATA_RETENTION_DOCS_URL} for supported data retention modes.`
+ : "";
+ if (error instanceof BedrockRuntimeServiceException) {
+ const prefix = BEDROCK_ERROR_PREFIXES[error.name] ?? error.name;
+ return `${prefix}: ${message}${dataRetentionHint}`;
+ }
+ return `${message}${dataRetentionHint}`;
+}
+
+/**
+ * Header keys that must never be overwritten by caller-supplied headers.
+ * `host` and `x-amz-*` participate in the SigV4 canonical request; `authorization`
+ * is owned by SigV4 or the bearer-token path (config.token + authSchemePreference).
+ * Compared case-insensitively (caller key is lower-cased before lookup).
+ */
+const RESERVED_HEADER_EXACT = new Set(["authorization", "host"]);
+
+function isReservedHeader(key: string): boolean {
+ const lower = key.toLowerCase();
+ return lower.startsWith("x-amz-") || RESERVED_HEADER_EXACT.has(lower);
+}
+
+/**
+ * Attach caller-supplied headers to the outgoing Bedrock request via a Smithy
+ * `build`-step middleware. The `build` step runs after request serialisation but
+ * before SigV4 signing, so injected headers are covered by the signature. Reserved
+ * SigV4 / auth headers (`x-amz-*`, `authorization`, `host`) are silently skipped;
+ * all other caller headers override any existing same-named header on the request.
+ */
+function addCustomHeadersMiddleware(client: BedrockRuntimeClient, headers: Record): void {
+ const middleware: BuildMiddleware = (next) => async (args) => {
+ const request = args.request;
+ if (request && typeof request === "object" && "headers" in request) {
+ const requestHeaders = (request as { headers: Record }).headers;
+ for (const [key, value] of Object.entries(headers)) {
+ if (!isReservedHeader(key)) {
+ requestHeaders[key] = value;
+ }
+ }
+ }
+ return next(args);
+ };
+ client.middlewareStack.add(middleware, { step: "build", name: "pi-ai-custom-headers", priority: "low" });
+}
+
+export const streamSimple: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions> = (
+ model: Model<"bedrock-converse-stream">,
+ context: Context,
+ options?: SimpleStreamOptions,
+): AssistantMessageEventStream => {
+ const base = buildBaseOptions(model, options, undefined);
+ if (!options?.reasoning) {
+ return stream(model, context, { ...base, reasoning: undefined } satisfies BedrockOptions);
+ }
+
+ if (isAnthropicClaudeModel(model)) {
+ if (supportsAdaptiveThinking(model.id, model.name)) {
+ return stream(model, context, {
+ ...base,
+ reasoning: options.reasoning,
+ thinkingBudgets: options.thinkingBudgets,
+ } satisfies BedrockOptions);
+ }
+
+ // Undefined means the caller did not request an output cap; let the helper use the model cap.
+ // Do not coerce to 0 here, or the thinking budget would become the entire maxTokens value.
+ const adjusted = adjustMaxTokensForThinking(
+ base.maxTokens,
+ model.maxTokens,
+ options.reasoning,
+ options.thinkingBudgets,
+ );
+
+ return stream(model, context, {
+ ...base,
+ maxTokens: adjusted.maxTokens,
+ reasoning: options.reasoning,
+ thinkingBudgets: {
+ ...(options.thinkingBudgets || {}),
+ [clampReasoning(options.reasoning)!]: adjusted.thinkingBudget,
+ },
+ } satisfies BedrockOptions);
+ }
+
+ return stream(model, context, {
+ ...base,
+ reasoning: options.reasoning,
+ thinkingBudgets: options.thinkingBudgets,
+ } satisfies BedrockOptions);
+};
+
+function handleContentBlockStart(
+ event: ContentBlockStartEvent,
+ blocks: Block[],
+ output: AssistantMessage,
+ stream: AssistantMessageEventStream,
+): void {
+ const index = event.contentBlockIndex!;
+ const start = event.start;
+
+ if (start?.toolUse) {
+ const block: Block = {
+ type: "toolCall",
+ id: start.toolUse.toolUseId || "",
+ name: start.toolUse.name || "",
+ arguments: {},
+ partialJson: "",
+ index,
+ };
+ output.content.push(block);
+ stream.push({ type: "toolcall_start", contentIndex: blocks.length - 1, partial: output });
+ }
+}
+
+function handleContentBlockDelta(
+ event: ContentBlockDeltaEvent,
+ blocks: Block[],
+ output: AssistantMessage,
+ stream: AssistantMessageEventStream,
+): void {
+ const contentBlockIndex = event.contentBlockIndex!;
+ const delta = event.delta;
+ let index = blocks.findIndex((b) => b.index === contentBlockIndex);
+ let block = blocks[index];
+
+ if (delta?.text !== undefined) {
+ // If no text block exists yet, create one, as `handleContentBlockStart` is not sent for text blocks
+ if (!block) {
+ const newBlock: Block = { type: "text", text: "", index: contentBlockIndex };
+ output.content.push(newBlock);
+ index = blocks.length - 1;
+ block = blocks[index];
+ stream.push({ type: "text_start", contentIndex: index, partial: output });
+ }
+ if (block.type === "text") {
+ block.text += delta.text;
+ stream.push({ type: "text_delta", contentIndex: index, delta: delta.text, partial: output });
+ }
+ } else if (delta?.toolUse && block?.type === "toolCall") {
+ block.partialJson = (block.partialJson || "") + (delta.toolUse.input || "");
+ block.arguments = parseStreamingJson(block.partialJson);
+ stream.push({ type: "toolcall_delta", contentIndex: index, delta: delta.toolUse.input || "", partial: output });
+ } else if (delta?.reasoningContent) {
+ let thinkingBlock = block;
+ let thinkingIndex = index;
+
+ if (!thinkingBlock) {
+ const newBlock: Block = { type: "thinking", thinking: "", thinkingSignature: "", index: contentBlockIndex };
+ output.content.push(newBlock);
+ thinkingIndex = blocks.length - 1;
+ thinkingBlock = blocks[thinkingIndex];
+ stream.push({ type: "thinking_start", contentIndex: thinkingIndex, partial: output });
+ }
+
+ if (thinkingBlock?.type === "thinking") {
+ if (delta.reasoningContent.text) {
+ thinkingBlock.thinking += delta.reasoningContent.text;
+ stream.push({
+ type: "thinking_delta",
+ contentIndex: thinkingIndex,
+ delta: delta.reasoningContent.text,
+ partial: output,
+ });
+ }
+ if (delta.reasoningContent.signature) {
+ thinkingBlock.thinkingSignature =
+ (thinkingBlock.thinkingSignature || "") + delta.reasoningContent.signature;
+ }
+ }
+ }
+}
+
+function handleMetadata(
+ event: ConverseStreamMetadataEvent,
+ model: Model<"bedrock-converse-stream">,
+ output: AssistantMessage,
+): void {
+ if (event.usage) {
+ output.usage.input = event.usage.inputTokens || 0;
+ output.usage.output = event.usage.outputTokens || 0;
+ output.usage.cacheRead = event.usage.cacheReadInputTokens || 0;
+ output.usage.cacheWrite = event.usage.cacheWriteInputTokens || 0;
+ output.usage.totalTokens = event.usage.totalTokens || output.usage.input + output.usage.output;
+ calculateCost(model, output.usage);
+ }
+}
+
+function handleContentBlockStop(
+ event: ContentBlockStopEvent,
+ blocks: Block[],
+ output: AssistantMessage,
+ stream: AssistantMessageEventStream,
+): void {
+ const index = blocks.findIndex((b) => b.index === event.contentBlockIndex);
+ const block = blocks[index];
+ if (!block) return;
+ delete (block as Block).index;
+
+ switch (block.type) {
+ case "text":
+ stream.push({ type: "text_end", contentIndex: index, content: block.text, partial: output });
+ break;
+ case "thinking":
+ stream.push({ type: "thinking_end", contentIndex: index, content: block.thinking, partial: output });
+ break;
+ case "toolCall":
+ block.arguments = parseStreamingJson(block.partialJson);
+ // Finalize in-place and strip the scratch buffer so replay only
+ // carries parsed arguments.
+ delete (block as Block).partialJson;
+ stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: output });
+ break;
+ }
+}
+
+/**
+ * Check if the model supports adaptive thinking (Opus 4.6+, Sonnet 4.6).
+ * Checks both model ID and model name to support application inference profiles
+ * whose ARNs don't contain the model name.
+ */
+function getModelMatchCandidates(modelId: string, modelName?: string): string[] {
+ const values = modelName ? [modelId, modelName] : [modelId];
+ return values.flatMap((value) => {
+ const lower = value.toLowerCase();
+ return [lower, lower.replace(/[\s_.:]+/g, "-")];
+ });
+}
+
+function supportsAdaptiveThinking(modelId: string, modelName?: string): boolean {
+ const candidates = getModelMatchCandidates(modelId, modelName);
+ return candidates.some(
+ (s) =>
+ s.includes("opus-4-6") ||
+ s.includes("opus-4-7") ||
+ s.includes("opus-4-8") ||
+ s.includes("sonnet-4-6") ||
+ s.includes("fable-5"),
+ );
+}
+
+function supportsNativeXhighEffort(model: Model<"bedrock-converse-stream">): boolean {
+ const candidates = getModelMatchCandidates(model.id, model.name);
+ return candidates.some((s) => s.includes("opus-4-7") || s.includes("opus-4-8") || s.includes("fable-5"));
+}
+
+function mapThinkingLevelToEffort(
+ model: Model<"bedrock-converse-stream">,
+ level: SimpleStreamOptions["reasoning"],
+): "low" | "medium" | "high" | "xhigh" | "max" {
+ if (level === "xhigh" && supportsNativeXhighEffort(model)) return "xhigh";
+
+ const mapped = level ? model.thinkingLevelMap?.[level] : undefined;
+ if (typeof mapped === "string") return mapped as "low" | "medium" | "high" | "xhigh" | "max";
+
+ switch (level) {
+ case "minimal":
+ case "low":
+ return "low";
+ case "medium":
+ return "medium";
+ case "high":
+ return "high";
+ default:
+ return "high";
+ }
+}
+
+/**
+ * Resolve cache retention preference.
+ * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility.
+ */
+function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention {
+ if (cacheRetention) {
+ return cacheRetention;
+ }
+ if (getProviderEnvValue("PI_CACHE_RETENTION", env) === "long") {
+ return "long";
+ }
+ return "short";
+}
+
+/**
+ * Check if the model is an Anthropic Claude model on Bedrock.
+ * Checks both model ID and model name to support application inference profiles
+ * whose ARNs don't contain the model name.
+ */
+function isAnthropicClaudeModel(model: Model<"bedrock-converse-stream">): boolean {
+ const id = model.id.toLowerCase();
+ const name = model.name?.toLowerCase() ?? "";
+ return (
+ id.includes("anthropic.claude") ||
+ id.includes("anthropic/claude") ||
+ name.includes("anthropic.claude") ||
+ name.includes("anthropic/claude") ||
+ name.includes("claude")
+ );
+}
+
+/**
+ * Check if the model supports prompt caching.
+ * Supported: Claude 3.5 Haiku, Claude 3.7 Sonnet, Claude 4.x models
+ *
+ * For base models and system-defined inference profiles the model ID / ARN
+ * contains the model name, so we can decide locally.
+ *
+ * For application inference profiles (whose ARNs don't contain the model name),
+ * also checks model.name which is user-controlled via models.json or registerProvider.
+ * As a last resort, set AWS_BEDROCK_FORCE_CACHE=1 to enable cache points.
+ * Amazon Nova models have automatic caching and don't need explicit cache points.
+ */
+function supportsPromptCaching(model: Model<"bedrock-converse-stream">, env?: ProviderEnv): boolean {
+ const candidates = getModelMatchCandidates(model.id, model.name);
+
+ const hasClaudeRef = candidates.some((s) => s.includes("claude"));
+ if (!hasClaudeRef) {
+ // Application inference profiles don't contain the model name in the ARN.
+ // Allow users to force cache points via environment variable.
+ if (getProviderEnvValue("AWS_BEDROCK_FORCE_CACHE", env) === "1") return true;
+ return false;
+ }
+ // Claude 4.x models (opus-4, sonnet-4, haiku-4)
+ if (candidates.some((s) => s.includes("-4-"))) return true;
+ // Claude 3.7 Sonnet
+ if (candidates.some((s) => s.includes("claude-3-7-sonnet"))) return true;
+ // Claude 3.5 Haiku
+ if (candidates.some((s) => s.includes("claude-3-5-haiku"))) return true;
+ return false;
+}
+
+/**
+ * Check if the model supports thinking signatures in reasoningContent.
+ * Only Anthropic Claude models support the signature field.
+ * Other models (OpenAI, Qwen, Minimax, Moonshot, etc.) reject it with:
+ * "This model doesn't support the reasoningContent.reasoningText.signature field"
+ *
+ * Checks both model ID and model name to support application inference profiles.
+ */
+function supportsThinkingSignature(model: Model<"bedrock-converse-stream">): boolean {
+ return isAnthropicClaudeModel(model);
+}
+
+function buildSystemPrompt(
+ systemPrompt: string | undefined,
+ model: Model<"bedrock-converse-stream">,
+ cacheRetention: CacheRetention,
+ env?: ProviderEnv,
+): SystemContentBlock[] | undefined {
+ if (!systemPrompt) return undefined;
+
+ const blocks: SystemContentBlock[] = [{ text: sanitizeSurrogates(systemPrompt) }];
+
+ // Add cache point for supported Claude models when caching is enabled
+ if (cacheRetention !== "none" && supportsPromptCaching(model, env)) {
+ blocks.push({
+ cachePoint: { type: CachePointType.DEFAULT, ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}) },
+ });
+ }
+
+ return blocks;
+}
+
+function normalizeToolCallId(id: string): string {
+ const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_");
+ return sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
+}
+
+function createNonBlankTextBlock(text: string): ContentBlock.TextMember | undefined {
+ const sanitized = sanitizeSurrogates(text);
+ return sanitized.trim().length === 0 ? undefined : { text: sanitized };
+}
+
+function createRequiredTextBlock(text: string): ContentBlock.TextMember {
+ return createNonBlankTextBlock(text) ?? { text: EMPTY_TEXT_PLACEHOLDER };
+}
+
+function convertToolResultContent(content: (TextContent | ImageContent)[]): ToolResultContentBlock[] {
+ const result: ToolResultContentBlock[] = [];
+ for (const c of content) {
+ if (c.type === "image") {
+ result.push({ image: createImageBlock(c.mimeType, c.data) });
+ } else {
+ const textBlock = createNonBlankTextBlock(c.text);
+ if (textBlock) result.push(textBlock);
+ }
+ }
+ if (result.length === 0) result.push({ text: EMPTY_TEXT_PLACEHOLDER });
+ return result;
+}
+
+function convertMessages(
+ context: Context,
+ model: Model<"bedrock-converse-stream">,
+ cacheRetention: CacheRetention,
+ env?: ProviderEnv,
+): Message[] {
+ const result: Message[] = [];
+ const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
+
+ for (let i = 0; i < transformedMessages.length; i++) {
+ const m = transformedMessages[i];
+
+ switch (m.role) {
+ case "user": {
+ const content: ContentBlock[] = [];
+ if (typeof m.content === "string") {
+ content.push(createRequiredTextBlock(m.content));
+ } else {
+ for (const c of m.content) {
+ switch (c.type) {
+ case "text": {
+ const textBlock = createNonBlankTextBlock(c.text);
+ if (textBlock) content.push(textBlock);
+ break;
+ }
+ case "image":
+ content.push({ image: createImageBlock(c.mimeType, c.data) });
+ break;
+ default:
+ continue;
+ }
+ }
+ if (content.length === 0) content.push({ text: EMPTY_TEXT_PLACEHOLDER });
+ }
+ result.push({
+ role: ConversationRole.USER,
+ content,
+ });
+ break;
+ }
+ case "assistant": {
+ // Skip assistant messages with empty content (e.g., from aborted requests)
+ // Bedrock rejects messages with empty content arrays
+ if (m.content.length === 0) {
+ continue;
+ }
+ const contentBlocks: ContentBlock[] = [];
+ for (const c of m.content) {
+ switch (c.type) {
+ case "text": {
+ // Skip empty text blocks
+ const textBlock = createNonBlankTextBlock(c.text);
+ if (!textBlock) continue;
+ contentBlocks.push(textBlock);
+ break;
+ }
+ case "toolCall":
+ contentBlocks.push({
+ toolUse: { toolUseId: c.id, name: c.name, input: c.arguments },
+ });
+ break;
+ case "thinking": {
+ // Skip empty thinking blocks
+ const thinking = sanitizeSurrogates(c.thinking);
+ if (thinking.trim().length === 0) continue;
+ // Only Anthropic models support the signature field in reasoningText.
+ // For other models, we omit the signature to avoid errors like:
+ // "This model doesn't support the reasoningContent.reasoningText.signature field"
+ if (supportsThinkingSignature(model)) {
+ // Signatures arrive after thinking deltas. If a partial or externally
+ // persisted message lacks a signature, Bedrock rejects the replayed
+ // reasoning block. Fall back to plain text, matching Anthropic.
+ if (!c.thinkingSignature || c.thinkingSignature.trim().length === 0) {
+ contentBlocks.push({ text: thinking });
+ } else {
+ contentBlocks.push({
+ reasoningContent: {
+ reasoningText: {
+ text: thinking,
+ signature: c.thinkingSignature,
+ },
+ },
+ });
+ }
+ } else {
+ contentBlocks.push({
+ reasoningContent: {
+ reasoningText: { text: thinking },
+ },
+ });
+ }
+ break;
+ }
+ default:
+ continue;
+ }
+ }
+ // Skip if all content blocks were filtered out
+ if (contentBlocks.length === 0) {
+ continue;
+ }
+ result.push({
+ role: ConversationRole.ASSISTANT,
+ content: contentBlocks,
+ });
+ break;
+ }
+ case "toolResult": {
+ // Collect all consecutive toolResult messages into a single user message
+ // Bedrock requires all tool results to be in one message
+ const toolResults: ContentBlock.ToolResultMember[] = [];
+
+ // Add current tool result with all content blocks combined
+ toolResults.push({
+ toolResult: {
+ toolUseId: m.toolCallId,
+ content: convertToolResultContent(m.content),
+ status: m.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS,
+ },
+ });
+
+ // Look ahead for consecutive toolResult messages
+ let j = i + 1;
+ while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
+ const nextMsg = transformedMessages[j] as ToolResultMessage;
+ toolResults.push({
+ toolResult: {
+ toolUseId: nextMsg.toolCallId,
+ content: convertToolResultContent(nextMsg.content),
+ status: nextMsg.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS,
+ },
+ });
+ j++;
+ }
+
+ // Skip the messages we've already processed
+ i = j - 1;
+
+ result.push({
+ role: ConversationRole.USER,
+ content: toolResults,
+ });
+ break;
+ }
+ default:
+ continue;
+ }
+ }
+
+ // Add cache point to the last user message for supported Claude models when caching is enabled
+ if (cacheRetention !== "none" && supportsPromptCaching(model, env) && result.length > 0) {
+ const lastMessage = result[result.length - 1];
+ if (lastMessage.role === ConversationRole.USER && lastMessage.content) {
+ (lastMessage.content as ContentBlock[]).push({
+ cachePoint: {
+ type: CachePointType.DEFAULT,
+ ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}),
+ },
+ });
+ }
+ }
+
+ return result;
+}
+
+function convertToolConfig(
+ tools: Tool[] | undefined,
+ toolChoice: BedrockOptions["toolChoice"],
+): ToolConfiguration | undefined {
+ if (!tools?.length || toolChoice === "none") return undefined;
+
+ const bedrockTools: BedrockTool[] = tools.map((tool) => ({
+ toolSpec: {
+ name: tool.name,
+ description: tool.description,
+ inputSchema: { json: tool.parameters as unknown as DocumentType },
+ },
+ }));
+
+ let bedrockToolChoice: ToolChoice | undefined;
+ switch (toolChoice) {
+ case "auto":
+ bedrockToolChoice = { auto: {} };
+ break;
+ case "any":
+ bedrockToolChoice = { any: {} };
+ break;
+ default:
+ if (toolChoice?.type === "tool") {
+ bedrockToolChoice = { tool: { name: toolChoice.name } };
+ }
+ }
+
+ return { tools: bedrockTools, toolChoice: bedrockToolChoice };
+}
+
+function mapStopReason(reason: string | undefined): StopReason {
+ switch (reason) {
+ case BedrockStopReason.END_TURN:
+ case BedrockStopReason.STOP_SEQUENCE:
+ return "stop";
+ case BedrockStopReason.MAX_TOKENS:
+ case BedrockStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED:
+ return "length";
+ case BedrockStopReason.TOOL_USE:
+ return "toolUse";
+ default:
+ return "error";
+ }
+}
+
+function getConfiguredBedrockRegion(options: BedrockOptions): string | undefined {
+ return (
+ options.region ||
+ getProviderEnvValue("AWS_REGION", options.env) ||
+ getProviderEnvValue("AWS_DEFAULT_REGION", options.env) ||
+ undefined
+ );
+}
+
+function getConfiguredBedrockCredentials(env?: ProviderEnv): BedrockRuntimeClientConfig["credentials"] | undefined {
+ const accessKeyId = getProviderEnvValue("AWS_ACCESS_KEY_ID", env);
+ const secretAccessKey = getProviderEnvValue("AWS_SECRET_ACCESS_KEY", env);
+ if (!accessKeyId || !secretAccessKey) {
+ return undefined;
+ }
+ const sessionToken = getProviderEnvValue("AWS_SESSION_TOKEN", env);
+ return {
+ accessKeyId,
+ secretAccessKey,
+ ...(sessionToken ? { sessionToken } : {}),
+ };
+}
+
+function getStandardBedrockEndpointRegion(baseUrl: string | undefined): string | undefined {
+ if (!baseUrl) {
+ return undefined;
+ }
+
+ try {
+ const { hostname } = new URL(baseUrl);
+ const match = hostname.toLowerCase().match(/^bedrock-runtime(?:-fips)?\.([a-z0-9-]+)\.amazonaws\.com(?:\.cn)?$/);
+ return match?.[1];
+ } catch {
+ return undefined;
+ }
+}
+
+function shouldUseExplicitBedrockEndpoint(
+ baseUrl: string,
+ configuredRegion: string | undefined,
+ hasAmbientConfiguredProfile: boolean,
+): boolean {
+ const endpointRegion = getStandardBedrockEndpointRegion(baseUrl);
+ if (!endpointRegion) {
+ return true;
+ }
+
+ return !configuredRegion && !hasAmbientConfiguredProfile;
+}
+
+function isGovCloudBedrockTarget(model: Model<"bedrock-converse-stream">, options: BedrockOptions): boolean {
+ const region = getConfiguredBedrockRegion(options);
+ if (region?.toLowerCase().startsWith("us-gov-")) {
+ return true;
+ }
+
+ const modelId = model.id.toLowerCase();
+ return modelId.startsWith("us-gov.") || modelId.startsWith("arn:aws-us-gov:");
+}
+
+function buildAdditionalModelRequestFields(
+ model: Model<"bedrock-converse-stream">,
+ options: BedrockOptions,
+): Record | undefined {
+ if (!options.reasoning || !model.reasoning) {
+ return undefined;
+ }
+
+ if (isAnthropicClaudeModel(model)) {
+ // GovCloud Bedrock currently rejects the Claude thinking.display field.
+ // Omit it there until the GovCloud Converse schema catches up.
+ const display = isGovCloudBedrockTarget(model, options) ? undefined : (options.thinkingDisplay ?? "summarized");
+ const result: Record = supportsAdaptiveThinking(model.id, model.name)
+ ? {
+ thinking: { type: "adaptive", ...(display !== undefined ? { display } : {}) },
+ output_config: { effort: mapThinkingLevelToEffort(model, options.reasoning) },
+ }
+ : (() => {
+ const defaultBudgets: Record = {
+ minimal: 1024,
+ low: 2048,
+ medium: 8192,
+ high: 16384,
+ xhigh: 16384, // Claude doesn't support xhigh, clamp to high
+ };
+
+ // Custom budgets override defaults (xhigh not in ThinkingBudgets, use high)
+ const level = options.reasoning === "xhigh" ? "high" : options.reasoning;
+ const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[options.reasoning];
+
+ return {
+ thinking: {
+ type: "enabled",
+ budget_tokens: budget,
+ ...(display !== undefined ? { display } : {}),
+ },
+ };
+ })();
+
+ if (!supportsAdaptiveThinking(model.id, model.name) && (options.interleavedThinking ?? true)) {
+ result.anthropic_beta = ["interleaved-thinking-2025-05-14"];
+ }
+
+ return result;
+ }
+
+ return undefined;
+}
+
+function createImageBlock(mimeType: string, data: string) {
+ let format: ImageFormat;
+ switch (mimeType) {
+ case "image/jpeg":
+ case "image/jpg":
+ format = ImageFormat.JPEG;
+ break;
+ case "image/png":
+ format = ImageFormat.PNG;
+ break;
+ case "image/gif":
+ format = ImageFormat.GIF;
+ break;
+ case "image/webp":
+ format = ImageFormat.WEBP;
+ break;
+ default:
+ throw new Error(`Unknown image type: ${mimeType}`);
+ }
+
+ const binaryString = atob(data);
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+
+ return { source: { bytes }, format };
+}
diff --git a/packages/ai/src/providers/cloudflare.ts b/packages/ai/src/api/cloudflare.ts
similarity index 50%
rename from packages/ai/src/providers/cloudflare.ts
rename to packages/ai/src/api/cloudflare.ts
index 98546419..f8b1138c 100644
--- a/packages/ai/src/providers/cloudflare.ts
+++ b/packages/ai/src/api/cloudflare.ts
@@ -1,6 +1,3 @@
-import type { Api, Model, ProviderEnv } from "../types.ts";
-import { getProviderEnvValue } from "../utils/provider-env.ts";
-
/** Workers AI direct endpoint. */
export const CLOUDFLARE_WORKERS_AI_BASE_URL =
"https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1";
@@ -16,21 +13,3 @@ export const CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL =
/** AI Gateway → Anthropic passthrough. */
export const CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL =
"https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic";
-
-export function isCloudflareProvider(provider: string): boolean {
- return provider === "cloudflare-workers-ai" || provider === "cloudflare-ai-gateway";
-}
-
-/** Substitute `{VAR}` placeholders in a Cloudflare baseUrl from provider env or process.env. */
-export function resolveCloudflareBaseUrl(model: Model, env?: ProviderEnv): string {
- const url = model.baseUrl;
- if (!url.includes("{")) return url;
- const baseUrl = url.replace(/\{([A-Z_][A-Z0-9_]*)\}/g, (_match, name: string) => {
- const value = getProviderEnvValue(name, env);
- if (!value) {
- throw new Error(`${name} is required for provider ${model.provider} but is not set.`);
- }
- return value;
- });
- return baseUrl;
-}
diff --git a/packages/ai/src/providers/github-copilot-headers.ts b/packages/ai/src/api/github-copilot-headers.ts
similarity index 100%
rename from packages/ai/src/providers/github-copilot-headers.ts
rename to packages/ai/src/api/github-copilot-headers.ts
diff --git a/packages/ai/src/api/google-generative-ai.lazy.ts b/packages/ai/src/api/google-generative-ai.lazy.ts
new file mode 100644
index 00000000..136c5043
--- /dev/null
+++ b/packages/ai/src/api/google-generative-ai.lazy.ts
@@ -0,0 +1,4 @@
+import type { ProviderStreams } from "../types.ts";
+import { lazyApi } from "./lazy.ts";
+
+export const googleGenerativeAIApi = (): ProviderStreams => lazyApi(() => import("./google-generative-ai.ts"));
diff --git a/packages/ai/src/api/google-generative-ai.ts b/packages/ai/src/api/google-generative-ai.ts
new file mode 100644
index 00000000..4e402a23
--- /dev/null
+++ b/packages/ai/src/api/google-generative-ai.ts
@@ -0,0 +1,507 @@
+import {
+ type GenerateContentConfig,
+ type GenerateContentParameters,
+ GoogleGenAI,
+ type ThinkingConfig,
+} from "@google/genai";
+import { calculateCost, clampThinkingLevel } from "../models.ts";
+import type {
+ Api,
+ AssistantMessage,
+ Context,
+ Model,
+ ProviderHeaders,
+ SimpleStreamOptions,
+ StreamFunction,
+ StreamOptions,
+ TextContent,
+ ThinkingBudgets,
+ ThinkingContent,
+ ThinkingLevel,
+ ToolCall,
+} from "../types.ts";
+import { AssistantMessageEventStream } from "../utils/event-stream.ts";
+import { providerHeadersToRecord } from "../utils/headers.ts";
+import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
+import type { GoogleThinkingLevel } from "./google-shared.ts";
+import {
+ convertMessages,
+ convertTools,
+ isThinkingPart,
+ mapStopReason,
+ mapToolChoice,
+ retainThoughtSignature,
+} from "./google-shared.ts";
+import { buildBaseOptions } from "./simple-options.ts";
+
+export interface GoogleOptions extends StreamOptions {
+ toolChoice?: "auto" | "none" | "any";
+ thinking?: {
+ enabled: boolean;
+ budgetTokens?: number; // -1 for dynamic, 0 to disable
+ level?: GoogleThinkingLevel;
+ };
+}
+
+// Counter for generating unique tool call IDs
+let toolCallCounter = 0;
+
+export const stream: StreamFunction<"google-generative-ai", GoogleOptions> = (
+ model: Model<"google-generative-ai">,
+ context: Context,
+ options?: GoogleOptions,
+): AssistantMessageEventStream => {
+ const stream = new AssistantMessageEventStream();
+
+ (async () => {
+ const output: AssistantMessage = {
+ role: "assistant",
+ content: [],
+ api: "google-generative-ai" as 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: "stop",
+ timestamp: Date.now(),
+ };
+
+ try {
+ const apiKey = options?.apiKey;
+ if (!apiKey) {
+ throw new Error(`No API key for provider: ${model.provider}`);
+ }
+ const client = createClient(model, apiKey, options?.headers);
+ let params = buildParams(model, context, options);
+ const nextParams = await options?.onPayload?.(params, model);
+ if (nextParams !== undefined) {
+ params = nextParams as GenerateContentParameters;
+ }
+ const googleStream = await client.models.generateContentStream(params);
+
+ stream.push({ type: "start", partial: output });
+ let currentBlock: TextContent | ThinkingContent | null = null;
+ const blocks = output.content;
+ const blockIndex = () => blocks.length - 1;
+ for await (const chunk of googleStream) {
+ // @google/genai documents GenerateContentResponse.responseId as an output-only field
+ // used to identify each response. Keep the first non-empty one from the stream.
+ output.responseId ||= chunk.responseId;
+ const candidate = chunk.candidates?.[0];
+ if (candidate?.content?.parts) {
+ for (const part of candidate.content.parts) {
+ if (part.text !== undefined) {
+ const isThinking = isThinkingPart(part);
+ if (
+ !currentBlock ||
+ (isThinking && currentBlock.type !== "thinking") ||
+ (!isThinking && currentBlock.type !== "text")
+ ) {
+ if (currentBlock) {
+ if (currentBlock.type === "text") {
+ stream.push({
+ type: "text_end",
+ contentIndex: blocks.length - 1,
+ content: currentBlock.text,
+ partial: output,
+ });
+ } else {
+ stream.push({
+ type: "thinking_end",
+ contentIndex: blockIndex(),
+ content: currentBlock.thinking,
+ partial: output,
+ });
+ }
+ }
+ if (isThinking) {
+ currentBlock = { type: "thinking", thinking: "", thinkingSignature: undefined };
+ output.content.push(currentBlock);
+ stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
+ } else {
+ currentBlock = { type: "text", text: "" };
+ output.content.push(currentBlock);
+ stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
+ }
+ }
+ if (currentBlock.type === "thinking") {
+ currentBlock.thinking += part.text;
+ currentBlock.thinkingSignature = retainThoughtSignature(
+ currentBlock.thinkingSignature,
+ part.thoughtSignature,
+ );
+ stream.push({
+ type: "thinking_delta",
+ contentIndex: blockIndex(),
+ delta: part.text,
+ partial: output,
+ });
+ } else {
+ currentBlock.text += part.text;
+ currentBlock.textSignature = retainThoughtSignature(
+ currentBlock.textSignature,
+ part.thoughtSignature,
+ );
+ stream.push({
+ type: "text_delta",
+ contentIndex: blockIndex(),
+ delta: part.text,
+ partial: output,
+ });
+ }
+ }
+
+ if (part.functionCall) {
+ if (currentBlock) {
+ if (currentBlock.type === "text") {
+ stream.push({
+ type: "text_end",
+ contentIndex: blockIndex(),
+ content: currentBlock.text,
+ partial: output,
+ });
+ } else {
+ stream.push({
+ type: "thinking_end",
+ contentIndex: blockIndex(),
+ content: currentBlock.thinking,
+ partial: output,
+ });
+ }
+ currentBlock = null;
+ }
+
+ // Generate unique ID if not provided or if it's a duplicate
+ const providedId = part.functionCall.id;
+ const needsNewId =
+ !providedId || output.content.some((b) => b.type === "toolCall" && b.id === providedId);
+ const toolCallId = needsNewId
+ ? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}`
+ : providedId;
+
+ const toolCall: ToolCall = {
+ type: "toolCall",
+ id: toolCallId,
+ name: part.functionCall.name || "",
+ arguments: (part.functionCall.args as Record) ?? {},
+ ...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }),
+ };
+
+ output.content.push(toolCall);
+ stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
+ stream.push({
+ type: "toolcall_delta",
+ contentIndex: blockIndex(),
+ delta: JSON.stringify(toolCall.arguments),
+ partial: output,
+ });
+ stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
+ }
+ }
+ }
+
+ if (candidate?.finishReason) {
+ output.stopReason = mapStopReason(candidate.finishReason);
+ if (output.content.some((b) => b.type === "toolCall")) {
+ output.stopReason = "toolUse";
+ }
+ }
+
+ if (chunk.usageMetadata) {
+ output.usage = {
+ input:
+ (chunk.usageMetadata.promptTokenCount || 0) - (chunk.usageMetadata.cachedContentTokenCount || 0),
+ output:
+ (chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
+ cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
+ cacheWrite: 0,
+ totalTokens: chunk.usageMetadata.totalTokenCount || 0,
+ cost: {
+ input: 0,
+ output: 0,
+ cacheRead: 0,
+ cacheWrite: 0,
+ total: 0,
+ },
+ };
+ calculateCost(model, output.usage);
+ }
+ }
+
+ if (currentBlock) {
+ if (currentBlock.type === "text") {
+ stream.push({
+ type: "text_end",
+ contentIndex: blockIndex(),
+ content: currentBlock.text,
+ partial: output,
+ });
+ } else {
+ stream.push({
+ type: "thinking_end",
+ contentIndex: blockIndex(),
+ content: currentBlock.thinking,
+ partial: output,
+ });
+ }
+ }
+
+ if (options?.signal?.aborted) {
+ throw new Error("Request was aborted");
+ }
+
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
+ throw new Error("An unknown error occurred");
+ }
+
+ stream.push({ type: "done", reason: output.stopReason, message: output });
+ stream.end();
+ } catch (error) {
+ // Remove internal index property used during streaming
+ for (const block of output.content) {
+ if ("index" in block) {
+ delete (block as { index?: number }).index;
+ }
+ }
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
+ output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
+ stream.push({ type: "error", reason: output.stopReason, error: output });
+ stream.end();
+ }
+ })();
+
+ return stream;
+};
+
+export const streamSimple: StreamFunction<"google-generative-ai", SimpleStreamOptions> = (
+ model: Model<"google-generative-ai">,
+ context: Context,
+ options?: SimpleStreamOptions,
+): AssistantMessageEventStream => {
+ const apiKey = options?.apiKey;
+ if (!apiKey) {
+ throw new Error(`No API key for provider: ${model.provider}`);
+ }
+
+ const base = buildBaseOptions(model, options, apiKey);
+ if (!options?.reasoning) {
+ return stream(model, context, { ...base, thinking: { enabled: false } } satisfies GoogleOptions);
+ }
+
+ const clampedReasoning = clampThinkingLevel(model, options.reasoning);
+ const effort = (clampedReasoning === "off" ? "high" : clampedReasoning) as ClampedThinkingLevel;
+ const googleModel = model as Model<"google-generative-ai">;
+
+ if (isGemini3ProModel(googleModel) || isGemini3FlashModel(googleModel) || isGemma4Model(googleModel)) {
+ return stream(model, context, {
+ ...base,
+ thinking: {
+ enabled: true,
+ level: getThinkingLevel(effort, googleModel),
+ },
+ } satisfies GoogleOptions);
+ }
+
+ return stream(model, context, {
+ ...base,
+ thinking: {
+ enabled: true,
+ budgetTokens: getGoogleBudget(googleModel, effort, options.thinkingBudgets),
+ },
+ } satisfies GoogleOptions);
+};
+
+function createClient(
+ model: Model<"google-generative-ai">,
+ apiKey?: string,
+ optionsHeaders?: ProviderHeaders,
+): GoogleGenAI {
+ const httpOptions: { baseUrl?: string; apiVersion?: string; headers?: Record } = {};
+ if (model.baseUrl) {
+ httpOptions.baseUrl = model.baseUrl;
+ httpOptions.apiVersion = ""; // baseUrl already includes version path, don't append
+ }
+ const headers = providerHeadersToRecord({ ...model.headers, ...optionsHeaders });
+ if (headers) {
+ httpOptions.headers = headers;
+ }
+
+ return new GoogleGenAI({
+ apiKey,
+ httpOptions: Object.keys(httpOptions).length > 0 ? httpOptions : undefined,
+ });
+}
+
+function buildParams(
+ model: Model<"google-generative-ai">,
+ context: Context,
+ options: GoogleOptions = {},
+): GenerateContentParameters {
+ const contents = convertMessages(model, context);
+
+ const generationConfig: GenerateContentConfig = {};
+ if (options.temperature !== undefined) {
+ generationConfig.temperature = options.temperature;
+ }
+ if (options.maxTokens !== undefined) {
+ generationConfig.maxOutputTokens = options.maxTokens;
+ }
+
+ const config: GenerateContentConfig = {
+ ...(Object.keys(generationConfig).length > 0 && generationConfig),
+ ...(context.systemPrompt && { systemInstruction: sanitizeSurrogates(context.systemPrompt) }),
+ ...(context.tools && context.tools.length > 0 && { tools: convertTools(context.tools) }),
+ };
+
+ if (context.tools && context.tools.length > 0 && options.toolChoice) {
+ config.toolConfig = {
+ functionCallingConfig: {
+ mode: mapToolChoice(options.toolChoice),
+ },
+ };
+ } else {
+ config.toolConfig = undefined;
+ }
+
+ if (options.thinking?.enabled && model.reasoning) {
+ const thinkingConfig: ThinkingConfig = { includeThoughts: true };
+ if (options.thinking.level !== undefined) {
+ // Cast to any since our GoogleThinkingLevel mirrors Google's ThinkingLevel enum values
+ thinkingConfig.thinkingLevel = options.thinking.level as any;
+ } else if (options.thinking.budgetTokens !== undefined) {
+ thinkingConfig.thinkingBudget = options.thinking.budgetTokens;
+ }
+ config.thinkingConfig = thinkingConfig;
+ } else if (model.reasoning && options.thinking && !options.thinking.enabled) {
+ config.thinkingConfig = getDisabledThinkingConfig(model);
+ }
+
+ if (options.signal) {
+ if (options.signal.aborted) {
+ throw new Error("Request aborted");
+ }
+ config.abortSignal = options.signal;
+ }
+
+ const params: GenerateContentParameters = {
+ model: model.id,
+ contents,
+ config,
+ };
+
+ return params;
+}
+
+type ClampedThinkingLevel = Exclude;
+
+function isGemma4Model(model: Model<"google-generative-ai">): boolean {
+ return /gemma-?4/.test(model.id.toLowerCase());
+}
+
+function isGemini3ProModel(model: Model<"google-generative-ai">): boolean {
+ return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase());
+}
+
+function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean {
+ const id = model.id.toLowerCase();
+ return /gemini-3(?:\.\d+)?-flash/.test(id) || id === "gemini-flash-latest" || id === "gemini-flash-lite-latest";
+}
+
+function getDisabledThinkingConfig(model: Model<"google-generative-ai">): ThinkingConfig {
+ // Google docs: Gemini 3.1 Pro cannot disable thinking, and Gemini 3 Flash / Flash-Lite
+ // do not support full thinking-off either. For Gemini 3 models, use the lowest supported
+ // thinkingLevel without includeThoughts so hidden thinking remains invisible to pi.
+ if (isGemini3ProModel(model)) {
+ return { thinkingLevel: "LOW" as any };
+ }
+ if (isGemini3FlashModel(model)) {
+ return { thinkingLevel: "MINIMAL" as any };
+ }
+ if (isGemma4Model(model)) {
+ return { thinkingLevel: "MINIMAL" as any };
+ }
+
+ // Gemini 2.x supports disabling via thinkingBudget = 0.
+ return { thinkingBudget: 0 };
+}
+
+function getThinkingLevel(effort: ClampedThinkingLevel, model: Model<"google-generative-ai">): GoogleThinkingLevel {
+ if (isGemini3ProModel(model)) {
+ switch (effort) {
+ case "minimal":
+ case "low":
+ return "LOW";
+ case "medium":
+ case "high":
+ return "HIGH";
+ }
+ }
+ if (isGemma4Model(model)) {
+ switch (effort) {
+ case "minimal":
+ case "low":
+ return "MINIMAL";
+ case "medium":
+ case "high":
+ return "HIGH";
+ }
+ }
+ switch (effort) {
+ case "minimal":
+ return "MINIMAL";
+ case "low":
+ return "LOW";
+ case "medium":
+ return "MEDIUM";
+ case "high":
+ return "HIGH";
+ }
+}
+
+function getGoogleBudget(
+ model: Model<"google-generative-ai">,
+ effort: ClampedThinkingLevel,
+ customBudgets?: ThinkingBudgets,
+): number {
+ if (customBudgets?.[effort] !== undefined) {
+ return customBudgets[effort]!;
+ }
+
+ if (model.id.includes("2.5-pro")) {
+ const budgets: Record = {
+ minimal: 128,
+ low: 2048,
+ medium: 8192,
+ high: 32768,
+ };
+ return budgets[effort];
+ }
+
+ if (model.id.includes("2.5-flash-lite")) {
+ const budgets: Record = {
+ minimal: 512,
+ low: 2048,
+ medium: 8192,
+ high: 24576,
+ };
+ return budgets[effort];
+ }
+
+ if (model.id.includes("2.5-flash")) {
+ const budgets: Record = {
+ minimal: 128,
+ low: 2048,
+ medium: 8192,
+ high: 24576,
+ };
+ return budgets[effort];
+ }
+
+ return -1;
+}
diff --git a/packages/ai/src/providers/google-shared.ts b/packages/ai/src/api/google-shared.ts
similarity index 100%
rename from packages/ai/src/providers/google-shared.ts
rename to packages/ai/src/api/google-shared.ts
diff --git a/packages/ai/src/api/google-vertex.lazy.ts b/packages/ai/src/api/google-vertex.lazy.ts
new file mode 100644
index 00000000..e79d4d0f
--- /dev/null
+++ b/packages/ai/src/api/google-vertex.lazy.ts
@@ -0,0 +1,4 @@
+import type { ProviderStreams } from "../types.ts";
+import { lazyApi } from "./lazy.ts";
+
+export const googleVertexApi = (): ProviderStreams => lazyApi(() => import("./google-vertex.ts"));
diff --git a/packages/ai/src/api/google-vertex.ts b/packages/ai/src/api/google-vertex.ts
new file mode 100644
index 00000000..de0ecd54
--- /dev/null
+++ b/packages/ai/src/api/google-vertex.ts
@@ -0,0 +1,582 @@
+import {
+ type GenerateContentConfig,
+ type GenerateContentParameters,
+ GoogleGenAI,
+ type HttpOptions,
+ ResourceScope,
+ type ThinkingConfig,
+ ThinkingLevel,
+} from "@google/genai";
+import { calculateCost, clampThinkingLevel } from "../models.ts";
+import type {
+ Api,
+ AssistantMessage,
+ Context,
+ Model,
+ ThinkingLevel as PiThinkingLevel,
+ ProviderEnv,
+ ProviderHeaders,
+ SimpleStreamOptions,
+ StreamFunction,
+ StreamOptions,
+ TextContent,
+ ThinkingBudgets,
+ ThinkingContent,
+ ToolCall,
+} from "../types.ts";
+import { AssistantMessageEventStream } from "../utils/event-stream.ts";
+import { providerHeadersToRecord } from "../utils/headers.ts";
+import { getProviderEnvValue } from "../utils/provider-env.ts";
+import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
+import type { GoogleThinkingLevel } from "./google-shared.ts";
+import {
+ convertMessages,
+ convertTools,
+ isThinkingPart,
+ mapStopReason,
+ mapToolChoice,
+ retainThoughtSignature,
+} from "./google-shared.ts";
+import { buildBaseOptions } from "./simple-options.ts";
+
+export interface GoogleVertexOptions extends StreamOptions {
+ toolChoice?: "auto" | "none" | "any";
+ thinking?: {
+ enabled: boolean;
+ budgetTokens?: number; // -1 for dynamic, 0 to disable
+ level?: GoogleThinkingLevel;
+ };
+ project?: string;
+ location?: string;
+}
+
+const API_VERSION = "v1";
+const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
+
+const THINKING_LEVEL_MAP: Record = {
+ THINKING_LEVEL_UNSPECIFIED: ThinkingLevel.THINKING_LEVEL_UNSPECIFIED,
+ MINIMAL: ThinkingLevel.MINIMAL,
+ LOW: ThinkingLevel.LOW,
+ MEDIUM: ThinkingLevel.MEDIUM,
+ HIGH: ThinkingLevel.HIGH,
+};
+
+// Counter for generating unique tool call IDs
+let toolCallCounter = 0;
+
+export const stream: StreamFunction<"google-vertex", GoogleVertexOptions> = (
+ model: Model<"google-vertex">,
+ context: Context,
+ options?: GoogleVertexOptions,
+): AssistantMessageEventStream => {
+ const stream = new AssistantMessageEventStream();
+
+ (async () => {
+ const output: AssistantMessage = {
+ role: "assistant",
+ content: [],
+ api: "google-vertex" as 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: "stop",
+ timestamp: Date.now(),
+ };
+
+ try {
+ const apiKey = resolveApiKey(options);
+ // Create the client using either a Vertex API key, if provided, or ADC with project and location
+ const client = apiKey
+ ? createClientWithApiKey(model, apiKey, options?.headers)
+ : createClient(model, resolveProject(options), resolveLocation(options), options?.headers, options?.env);
+ let params = buildParams(model, context, options);
+ const nextParams = await options?.onPayload?.(params, model);
+ if (nextParams !== undefined) {
+ params = nextParams as GenerateContentParameters;
+ }
+ const googleStream = await client.models.generateContentStream(params);
+
+ stream.push({ type: "start", partial: output });
+ let currentBlock: TextContent | ThinkingContent | null = null;
+ const blocks = output.content;
+ const blockIndex = () => blocks.length - 1;
+ for await (const chunk of googleStream) {
+ // Vertex uses the same @google/genai GenerateContentResponse type as Gemini.
+ // responseId is documented there as an output-only identifier for each response.
+ output.responseId ||= chunk.responseId;
+ const candidate = chunk.candidates?.[0];
+ if (candidate?.content?.parts) {
+ for (const part of candidate.content.parts) {
+ if (part.text !== undefined) {
+ const isThinking = isThinkingPart(part);
+ if (
+ !currentBlock ||
+ (isThinking && currentBlock.type !== "thinking") ||
+ (!isThinking && currentBlock.type !== "text")
+ ) {
+ if (currentBlock) {
+ if (currentBlock.type === "text") {
+ stream.push({
+ type: "text_end",
+ contentIndex: blocks.length - 1,
+ content: currentBlock.text,
+ partial: output,
+ });
+ } else {
+ stream.push({
+ type: "thinking_end",
+ contentIndex: blockIndex(),
+ content: currentBlock.thinking,
+ partial: output,
+ });
+ }
+ }
+ if (isThinking) {
+ currentBlock = { type: "thinking", thinking: "", thinkingSignature: undefined };
+ output.content.push(currentBlock);
+ stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
+ } else {
+ currentBlock = { type: "text", text: "" };
+ output.content.push(currentBlock);
+ stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
+ }
+ }
+ if (currentBlock.type === "thinking") {
+ currentBlock.thinking += part.text;
+ currentBlock.thinkingSignature = retainThoughtSignature(
+ currentBlock.thinkingSignature,
+ part.thoughtSignature,
+ );
+ stream.push({
+ type: "thinking_delta",
+ contentIndex: blockIndex(),
+ delta: part.text,
+ partial: output,
+ });
+ } else {
+ currentBlock.text += part.text;
+ currentBlock.textSignature = retainThoughtSignature(
+ currentBlock.textSignature,
+ part.thoughtSignature,
+ );
+ stream.push({
+ type: "text_delta",
+ contentIndex: blockIndex(),
+ delta: part.text,
+ partial: output,
+ });
+ }
+ }
+
+ if (part.functionCall) {
+ if (currentBlock) {
+ if (currentBlock.type === "text") {
+ stream.push({
+ type: "text_end",
+ contentIndex: blockIndex(),
+ content: currentBlock.text,
+ partial: output,
+ });
+ } else {
+ stream.push({
+ type: "thinking_end",
+ contentIndex: blockIndex(),
+ content: currentBlock.thinking,
+ partial: output,
+ });
+ }
+ currentBlock = null;
+ }
+
+ const providedId = part.functionCall.id;
+ const needsNewId =
+ !providedId || output.content.some((b) => b.type === "toolCall" && b.id === providedId);
+ const toolCallId = needsNewId
+ ? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}`
+ : providedId;
+
+ const toolCall: ToolCall = {
+ type: "toolCall",
+ id: toolCallId,
+ name: part.functionCall.name || "",
+ arguments: (part.functionCall.args as Record) ?? {},
+ ...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }),
+ };
+
+ output.content.push(toolCall);
+ stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
+ stream.push({
+ type: "toolcall_delta",
+ contentIndex: blockIndex(),
+ delta: JSON.stringify(toolCall.arguments),
+ partial: output,
+ });
+ stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
+ }
+ }
+ }
+
+ if (candidate?.finishReason) {
+ output.stopReason = mapStopReason(candidate.finishReason);
+ if (output.content.some((b) => b.type === "toolCall")) {
+ output.stopReason = "toolUse";
+ }
+ }
+
+ if (chunk.usageMetadata) {
+ output.usage = {
+ input:
+ (chunk.usageMetadata.promptTokenCount || 0) - (chunk.usageMetadata.cachedContentTokenCount || 0),
+ output:
+ (chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
+ cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
+ cacheWrite: 0,
+ totalTokens: chunk.usageMetadata.totalTokenCount || 0,
+ cost: {
+ input: 0,
+ output: 0,
+ cacheRead: 0,
+ cacheWrite: 0,
+ total: 0,
+ },
+ };
+ calculateCost(model, output.usage);
+ }
+ }
+
+ if (currentBlock) {
+ if (currentBlock.type === "text") {
+ stream.push({
+ type: "text_end",
+ contentIndex: blockIndex(),
+ content: currentBlock.text,
+ partial: output,
+ });
+ } else {
+ stream.push({
+ type: "thinking_end",
+ contentIndex: blockIndex(),
+ content: currentBlock.thinking,
+ partial: output,
+ });
+ }
+ }
+
+ if (options?.signal?.aborted) {
+ throw new Error("Request was aborted");
+ }
+
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
+ throw new Error("An unknown error occurred");
+ }
+
+ stream.push({ type: "done", reason: output.stopReason, message: output });
+ stream.end();
+ } catch (error) {
+ // Remove internal index property used during streaming
+ for (const block of output.content) {
+ if ("index" in block) {
+ delete (block as { index?: number }).index;
+ }
+ }
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
+ output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
+ stream.push({ type: "error", reason: output.stopReason, error: output });
+ stream.end();
+ }
+ })();
+
+ return stream;
+};
+
+export const streamSimple: StreamFunction<"google-vertex", SimpleStreamOptions> = (
+ model: Model<"google-vertex">,
+ context: Context,
+ options?: SimpleStreamOptions,
+): AssistantMessageEventStream => {
+ const base = buildBaseOptions(model, options, undefined);
+ if (!options?.reasoning) {
+ return stream(model, context, {
+ ...base,
+ thinking: { enabled: false },
+ } satisfies GoogleVertexOptions);
+ }
+
+ const clampedReasoning = clampThinkingLevel(model, options.reasoning);
+ const effort = (clampedReasoning === "off" ? "high" : clampedReasoning) as ClampedThinkingLevel;
+ const geminiModel = model as unknown as Model<"google-generative-ai">;
+
+ if (isGemini3ProModel(geminiModel) || isGemini3FlashModel(geminiModel)) {
+ return stream(model, context, {
+ ...base,
+ thinking: {
+ enabled: true,
+ level: getGemini3ThinkingLevel(effort, geminiModel),
+ },
+ } satisfies GoogleVertexOptions);
+ }
+
+ return stream(model, context, {
+ ...base,
+ thinking: {
+ enabled: true,
+ budgetTokens: getGoogleBudget(geminiModel, effort, options.thinkingBudgets),
+ },
+ } satisfies GoogleVertexOptions);
+};
+
+function createClient(
+ model: Model<"google-vertex">,
+ project: string,
+ location: string,
+ optionsHeaders?: ProviderHeaders,
+ env?: ProviderEnv,
+): GoogleGenAI {
+ const googleAuthOptions = buildGoogleAuthOptions(env);
+ return new GoogleGenAI({
+ vertexai: true,
+ project,
+ location,
+ apiVersion: API_VERSION,
+ ...(googleAuthOptions ? { googleAuthOptions } : {}),
+ httpOptions: buildHttpOptions(model, optionsHeaders),
+ });
+}
+
+function createClientWithApiKey(
+ model: Model<"google-vertex">,
+ apiKey: string,
+ optionsHeaders?: ProviderHeaders,
+): GoogleGenAI {
+ return new GoogleGenAI({
+ vertexai: true,
+ apiKey,
+ apiVersion: API_VERSION,
+ httpOptions: buildHttpOptions(model, optionsHeaders),
+ });
+}
+
+function buildHttpOptions(model: Model<"google-vertex">, optionsHeaders?: ProviderHeaders): HttpOptions | undefined {
+ const httpOptions: HttpOptions = {};
+ const baseUrl = resolveCustomBaseUrl(model.baseUrl);
+ if (baseUrl) {
+ httpOptions.baseUrl = baseUrl;
+ httpOptions.baseUrlResourceScope = ResourceScope.COLLECTION;
+ if (baseUrlIncludesApiVersion(baseUrl)) {
+ httpOptions.apiVersion = "";
+ }
+ }
+
+ const headers = providerHeadersToRecord({ ...model.headers, ...optionsHeaders });
+ if (headers) {
+ httpOptions.headers = headers;
+ }
+
+ return Object.keys(httpOptions).length > 0 ? httpOptions : undefined;
+}
+
+function resolveCustomBaseUrl(baseUrl: string): string | undefined {
+ const trimmed = baseUrl.trim();
+ if (!trimmed || trimmed.includes("{location}")) {
+ return undefined;
+ }
+ return trimmed;
+}
+
+function baseUrlIncludesApiVersion(baseUrl: string): boolean {
+ try {
+ const url = new URL(baseUrl);
+ return url.pathname.split("/").some((part) => /^v\d+(?:beta\d*)?$/.test(part));
+ } catch {
+ return /(?:^|\/)v\d+(?:beta\d*)?(?:\/|$)/.test(baseUrl);
+ }
+}
+
+function buildGoogleAuthOptions(env?: ProviderEnv): { keyFilename: string } | undefined {
+ const keyFilename = getProviderEnvValue("GOOGLE_APPLICATION_CREDENTIALS", env);
+ return keyFilename ? { keyFilename } : undefined;
+}
+
+function resolveApiKey(options?: GoogleVertexOptions): string | undefined {
+ const apiKey = options?.apiKey?.trim();
+ if (!apiKey || apiKey === GCP_VERTEX_CREDENTIALS_MARKER || isPlaceholderApiKey(apiKey)) {
+ return undefined;
+ }
+ return apiKey;
+}
+
+function isPlaceholderApiKey(apiKey: string): boolean {
+ return /^<[^>]+>$/.test(apiKey);
+}
+
+function resolveProject(options?: GoogleVertexOptions): string {
+ const project =
+ options?.project ||
+ getProviderEnvValue("GOOGLE_CLOUD_PROJECT", options?.env) ||
+ getProviderEnvValue("GCLOUD_PROJECT", options?.env);
+ if (!project) {
+ throw new Error(
+ "Vertex AI requires a project ID. Set GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT or pass project in options.",
+ );
+ }
+ return project;
+}
+
+function resolveLocation(options?: GoogleVertexOptions): string {
+ const location = options?.location || getProviderEnvValue("GOOGLE_CLOUD_LOCATION", options?.env);
+ if (!location) {
+ throw new Error("Vertex AI requires a location. Set GOOGLE_CLOUD_LOCATION or pass location in options.");
+ }
+ return location;
+}
+
+function buildParams(
+ model: Model<"google-vertex">,
+ context: Context,
+ options: GoogleVertexOptions = {},
+): GenerateContentParameters {
+ const contents = convertMessages(model, context);
+
+ const generationConfig: GenerateContentConfig = {};
+ if (options.temperature !== undefined) {
+ generationConfig.temperature = options.temperature;
+ }
+ if (options.maxTokens !== undefined) {
+ generationConfig.maxOutputTokens = options.maxTokens;
+ }
+
+ const config: GenerateContentConfig = {
+ ...(Object.keys(generationConfig).length > 0 && generationConfig),
+ ...(context.systemPrompt && { systemInstruction: sanitizeSurrogates(context.systemPrompt) }),
+ ...(context.tools && context.tools.length > 0 && { tools: convertTools(context.tools) }),
+ };
+
+ if (context.tools && context.tools.length > 0 && options.toolChoice) {
+ config.toolConfig = {
+ functionCallingConfig: {
+ mode: mapToolChoice(options.toolChoice),
+ },
+ };
+ } else {
+ config.toolConfig = undefined;
+ }
+
+ if (options.thinking?.enabled && model.reasoning) {
+ const thinkingConfig: ThinkingConfig = { includeThoughts: true };
+ if (options.thinking.level !== undefined) {
+ thinkingConfig.thinkingLevel = THINKING_LEVEL_MAP[options.thinking.level];
+ } else if (options.thinking.budgetTokens !== undefined) {
+ thinkingConfig.thinkingBudget = options.thinking.budgetTokens;
+ }
+ config.thinkingConfig = thinkingConfig;
+ } else if (model.reasoning && options.thinking && !options.thinking.enabled) {
+ config.thinkingConfig = getDisabledThinkingConfig(model);
+ }
+
+ if (options.signal) {
+ if (options.signal.aborted) {
+ throw new Error("Request aborted");
+ }
+ config.abortSignal = options.signal;
+ }
+
+ const params: GenerateContentParameters = {
+ model: model.id,
+ contents,
+ config,
+ };
+
+ return params;
+}
+
+type ClampedThinkingLevel = Exclude;
+
+function isGemini3ProModel(model: Model<"google-generative-ai">): boolean {
+ return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase());
+}
+
+function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean {
+ const id = model.id.toLowerCase();
+ return /gemini-3(?:\.\d+)?-flash/.test(id) || id === "gemini-flash-latest" || id === "gemini-flash-lite-latest";
+}
+
+function getDisabledThinkingConfig(model: Model<"google-vertex">): ThinkingConfig {
+ // Google docs: Gemini 3.1 Pro cannot disable thinking, and Gemini 3 Flash / Flash-Lite
+ // do not support full thinking-off either. For Gemini 3 models, use the lowest supported
+ // thinkingLevel without includeThoughts so hidden thinking remains invisible to pi.
+ const geminiModel = model as unknown as Model<"google-generative-ai">;
+ if (isGemini3ProModel(geminiModel)) {
+ return { thinkingLevel: ThinkingLevel.LOW };
+ }
+ if (isGemini3FlashModel(geminiModel)) {
+ return { thinkingLevel: ThinkingLevel.MINIMAL };
+ }
+
+ // Gemini 2.x supports disabling via thinkingBudget = 0.
+ return { thinkingBudget: 0 };
+}
+
+function getGemini3ThinkingLevel(
+ effort: ClampedThinkingLevel,
+ model: Model<"google-generative-ai">,
+): GoogleThinkingLevel {
+ if (isGemini3ProModel(model)) {
+ switch (effort) {
+ case "minimal":
+ case "low":
+ return "LOW";
+ case "medium":
+ case "high":
+ return "HIGH";
+ }
+ }
+ switch (effort) {
+ case "minimal":
+ return "MINIMAL";
+ case "low":
+ return "LOW";
+ case "medium":
+ return "MEDIUM";
+ case "high":
+ return "HIGH";
+ }
+}
+
+function getGoogleBudget(
+ model: Model<"google-generative-ai">,
+ effort: ClampedThinkingLevel,
+ customBudgets?: ThinkingBudgets,
+): number {
+ if (customBudgets?.[effort] !== undefined) {
+ return customBudgets[effort]!;
+ }
+
+ if (model.id.includes("2.5-pro")) {
+ const budgets: Record = {
+ minimal: 128,
+ low: 2048,
+ medium: 8192,
+ high: 32768,
+ };
+ return budgets[effort];
+ }
+
+ if (model.id.includes("2.5-flash")) {
+ const budgets: Record = {
+ minimal: 128,
+ low: 2048,
+ medium: 8192,
+ high: 24576,
+ };
+ return budgets[effort];
+ }
+
+ return -1;
+}
diff --git a/packages/ai/src/api/lazy.ts b/packages/ai/src/api/lazy.ts
new file mode 100644
index 00000000..fe1836ae
--- /dev/null
+++ b/packages/ai/src/api/lazy.ts
@@ -0,0 +1,70 @@
+import type { Api, AssistantMessage, AssistantMessageEvent, Model, ProviderStreams } from "../types.ts";
+import { AssistantMessageEventStream } from "../utils/event-stream.ts";
+
+function createSetupErrorMessage(model: Model, 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(),
+ };
+}
+
+function forwardStream(target: AssistantMessageEventStream, source: AsyncIterable): void {
+ (async () => {
+ for await (const event of source) {
+ target.push(event);
+ }
+ target.end();
+ })();
+}
+
+/**
+ * Returns a stream synchronously while running async setup (auth resolution,
+ * lazy module loading) behind it. Setup failures terminate the stream with an
+ * error event.
+ */
+export function lazyStream(
+ model: Model,
+ setup: () => Promise>,
+): AssistantMessageEventStream {
+ const outer = new AssistantMessageEventStream();
+
+ setup()
+ .then((inner) => {
+ forwardStream(outer, inner);
+ })
+ .catch((error) => {
+ const message = createSetupErrorMessage(model, error);
+ outer.push({ type: "error", reason: "error", error: message });
+ outer.end(message);
+ });
+
+ return outer;
+}
+
+/**
+ * Wraps a dynamically imported API implementation module as `ProviderStreams`.
+ * The module loads on first stream call; the host's import cache deduplicates
+ * loads. Load failures terminate the returned stream with an error event.
+ */
+export function lazyApi(load: () => Promise): ProviderStreams {
+ return {
+ stream: (model, context, options) =>
+ lazyStream(model, async () => (await load()).stream(model, context, options)),
+ streamSimple: (model, context, options) =>
+ lazyStream(model, async () => (await load()).streamSimple(model, context, options)),
+ };
+}
diff --git a/packages/ai/src/api/mistral-conversations.lazy.ts b/packages/ai/src/api/mistral-conversations.lazy.ts
new file mode 100644
index 00000000..84fd03ff
--- /dev/null
+++ b/packages/ai/src/api/mistral-conversations.lazy.ts
@@ -0,0 +1,4 @@
+import type { ProviderStreams } from "../types.ts";
+import { lazyApi } from "./lazy.ts";
+
+export const mistralConversationsApi = (): ProviderStreams => lazyApi(() => import("./mistral-conversations.ts"));
diff --git a/packages/ai/src/api/mistral-conversations.ts b/packages/ai/src/api/mistral-conversations.ts
new file mode 100644
index 00000000..66519e5e
--- /dev/null
+++ b/packages/ai/src/api/mistral-conversations.ts
@@ -0,0 +1,664 @@
+import { Mistral } from "@mistralai/mistralai";
+import type {
+ ChatCompletionStreamRequest,
+ ChatCompletionStreamRequestMessage,
+ CompletionEvent,
+ ContentChunk,
+ FunctionTool,
+} from "@mistralai/mistralai/models/components";
+import { calculateCost, clampThinkingLevel } from "../models.ts";
+import type {
+ AssistantMessage,
+ Context,
+ Message,
+ Model,
+ SimpleStreamOptions,
+ StopReason,
+ StreamFunction,
+ StreamOptions,
+ TextContent,
+ ThinkingContent,
+ Tool,
+ ToolCall,
+} from "../types.ts";
+import { AssistantMessageEventStream } from "../utils/event-stream.ts";
+import { shortHash } from "../utils/hash.ts";
+import { parseStreamingJson } from "../utils/json-parse.ts";
+import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
+import { buildBaseOptions } from "./simple-options.ts";
+import { transformMessages } from "./transform-messages.ts";
+
+const MISTRAL_TOOL_CALL_ID_LENGTH = 9;
+const MAX_MISTRAL_ERROR_BODY_CHARS = 4000;
+
+/**
+ * Provider-specific options for the Mistral API.
+ */
+type MistralReasoningEffort = "none" | "high";
+
+export interface MistralOptions extends StreamOptions {
+ toolChoice?: "auto" | "none" | "any" | "required" | { type: "function"; function: { name: string } };
+ promptMode?: "reasoning";
+ reasoningEffort?: MistralReasoningEffort;
+}
+
+/**
+ * Stream responses from Mistral using `chat.stream`.
+ */
+export const stream: StreamFunction<"mistral-conversations", MistralOptions> = (
+ model: Model<"mistral-conversations">,
+ context: Context,
+ options?: MistralOptions,
+): AssistantMessageEventStream => {
+ const stream = new AssistantMessageEventStream();
+
+ (async () => {
+ const output = createOutput(model);
+
+ try {
+ const apiKey = options?.apiKey;
+ if (!apiKey) {
+ throw new Error(`No API key for provider: ${model.provider}`);
+ }
+
+ // Intentionally per-request: avoids shared SDK mutable state across concurrent consumers.
+ const mistral = new Mistral({
+ apiKey,
+ serverURL: model.baseUrl,
+ });
+
+ const normalizeMistralToolCallId = createMistralToolCallIdNormalizer();
+ const transformedMessages = transformMessages(context.messages, model, (id) => normalizeMistralToolCallId(id));
+
+ let payload = buildChatPayload(model, context, transformedMessages, options);
+ const nextPayload = await options?.onPayload?.(payload, model);
+ if (nextPayload !== undefined) {
+ payload = nextPayload as ChatCompletionStreamRequest;
+ }
+ const mistralStream = await mistral.chat.stream(payload, buildRequestOptions(model, options));
+ stream.push({ type: "start", partial: output });
+ await consumeChatStream(model, output, stream, mistralStream);
+
+ if (options?.signal?.aborted) {
+ throw new Error("Request was aborted");
+ }
+
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
+ throw new Error("An unknown error occurred");
+ }
+
+ stream.push({ type: "done", reason: output.stopReason, message: output });
+ stream.end();
+ } catch (error) {
+ for (const block of output.content) {
+ // partialArgs is only a streaming scratch buffer; never persist it.
+ delete (block as { partialArgs?: string }).partialArgs;
+ }
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
+ output.errorMessage = formatMistralError(error);
+ stream.push({ type: "error", reason: output.stopReason, error: output });
+ stream.end();
+ }
+ })();
+
+ return stream;
+};
+
+/**
+ * Maps provider-agnostic `SimpleStreamOptions` to Mistral options.
+ */
+export const streamSimple: StreamFunction<"mistral-conversations", SimpleStreamOptions> = (
+ model: Model<"mistral-conversations">,
+ context: Context,
+ options?: SimpleStreamOptions,
+): AssistantMessageEventStream => {
+ const apiKey = options?.apiKey;
+ if (!apiKey) {
+ throw new Error(`No API key for provider: ${model.provider}`);
+ }
+
+ const base = buildBaseOptions(model, options, apiKey);
+ const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
+ const reasoning = clampedReasoning === "off" ? undefined : clampedReasoning;
+ const shouldUseReasoning = model.reasoning && reasoning !== undefined;
+
+ return stream(model, context, {
+ ...base,
+ promptMode: shouldUseReasoning && usesPromptModeReasoning(model) ? "reasoning" : undefined,
+ reasoningEffort:
+ shouldUseReasoning && usesReasoningEffort(model) ? mapReasoningEffort(model, reasoning) : undefined,
+ } satisfies MistralOptions);
+};
+
+function createOutput(model: Model<"mistral-conversations">): 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: "stop",
+ timestamp: Date.now(),
+ };
+}
+
+function createMistralToolCallIdNormalizer(): (id: string) => string {
+ const idMap = new Map();
+ const reverseMap = new Map();
+
+ return (id: string): string => {
+ const existing = idMap.get(id);
+ if (existing) return existing;
+
+ let attempt = 0;
+ while (true) {
+ const candidate = deriveMistralToolCallId(id, attempt);
+ const owner = reverseMap.get(candidate);
+ if (!owner || owner === id) {
+ idMap.set(id, candidate);
+ reverseMap.set(candidate, id);
+ return candidate;
+ }
+ attempt++;
+ }
+ };
+}
+
+function deriveMistralToolCallId(id: string, attempt: number): string {
+ const normalized = id.replace(/[^a-zA-Z0-9]/g, "");
+ if (attempt === 0 && normalized.length === MISTRAL_TOOL_CALL_ID_LENGTH) return normalized;
+ const seedBase = normalized || id;
+ const seed = attempt === 0 ? seedBase : `${seedBase}:${attempt}`;
+ return shortHash(seed)
+ .replace(/[^a-zA-Z0-9]/g, "")
+ .slice(0, MISTRAL_TOOL_CALL_ID_LENGTH);
+}
+
+function formatMistralError(error: unknown): string {
+ if (error instanceof Error) {
+ const sdkError = error as Error & { statusCode?: unknown; body?: unknown };
+ const statusCode = typeof sdkError.statusCode === "number" ? sdkError.statusCode : undefined;
+ const bodyText = typeof sdkError.body === "string" ? sdkError.body.trim() : undefined;
+ if (statusCode !== undefined && bodyText) {
+ return `Mistral API error (${statusCode}): ${truncateErrorText(bodyText, MAX_MISTRAL_ERROR_BODY_CHARS)}`;
+ }
+ if (statusCode !== undefined) return `Mistral API error (${statusCode}): ${error.message}`;
+ return error.message;
+ }
+ return safeJsonStringify(error);
+}
+
+function truncateErrorText(text: string, maxChars: number): string {
+ if (text.length <= maxChars) return text;
+ return `${text.slice(0, maxChars)}... [truncated ${text.length - maxChars} chars]`;
+}
+
+function safeJsonStringify(value: unknown): string {
+ try {
+ const serialized = JSON.stringify(value);
+ return serialized === undefined ? String(value) : serialized;
+ } catch {
+ return String(value);
+ }
+}
+
+function buildRequestOptions(model: Model<"mistral-conversations">, options?: MistralOptions) {
+ const requestOptions: {
+ signal?: AbortSignal;
+ retries: { strategy: "none" };
+ headers?: Record;
+ } = {
+ retries: { strategy: "none" },
+ };
+ if (options?.signal) requestOptions.signal = options.signal;
+
+ const headers: Record = {};
+ if (model.headers) Object.assign(headers, model.headers);
+ if (options?.headers) Object.assign(headers, options.headers);
+
+ // Mistral infrastructure uses `x-affinity` for KV-cache reuse (prefix caching).
+ // Respect explicit caller-provided header values.
+ if (shouldUsePromptCaching(options) && !headers["x-affinity"]) {
+ headers["x-affinity"] = options.sessionId;
+ }
+
+ if (Object.keys(headers).length > 0) {
+ requestOptions.headers = headers;
+ }
+
+ return requestOptions;
+}
+
+function buildChatPayload(
+ model: Model<"mistral-conversations">,
+ context: Context,
+ messages: Message[],
+ options?: MistralOptions,
+): ChatCompletionStreamRequest {
+ const payload: ChatCompletionStreamRequest = {
+ model: model.id,
+ stream: true,
+ messages: toChatMessages(messages, model.input.includes("image")),
+ };
+
+ if (context.tools?.length) payload.tools = toFunctionTools(context.tools);
+ if (options?.temperature !== undefined) payload.temperature = options.temperature;
+ if (options?.maxTokens !== undefined) payload.maxTokens = options.maxTokens;
+ if (options?.toolChoice) payload.toolChoice = mapToolChoice(options.toolChoice);
+ if (options?.promptMode) payload.promptMode = options.promptMode;
+ if (options?.reasoningEffort) payload.reasoningEffort = options.reasoningEffort;
+ if (shouldUsePromptCaching(options)) payload.promptCacheKey = options.sessionId;
+
+ if (context.systemPrompt) {
+ payload.messages.unshift({
+ role: "system",
+ content: sanitizeSurrogates(context.systemPrompt),
+ });
+ }
+
+ return payload;
+}
+
+function shouldUsePromptCaching(options?: MistralOptions): options is MistralOptions & { sessionId: string } {
+ return options?.cacheRetention !== "none" && !!options?.sessionId;
+}
+
+function getMistralCachedPromptTokens(usage: unknown, promptTokens: number): number {
+ const rawUsage = usage as {
+ promptTokensDetails?: { cachedTokens?: unknown } | null;
+ prompt_tokens_details?: { cached_tokens?: unknown } | null;
+ promptTokenDetails?: { cachedTokens?: unknown } | null;
+ prompt_token_details?: { cached_tokens?: unknown } | null;
+ numCachedTokens?: unknown;
+ num_cached_tokens?: unknown;
+ };
+ const rawCachedTokens =
+ rawUsage.promptTokensDetails?.cachedTokens ??
+ rawUsage.prompt_tokens_details?.cached_tokens ??
+ rawUsage.promptTokenDetails?.cachedTokens ??
+ rawUsage.prompt_token_details?.cached_tokens ??
+ rawUsage.numCachedTokens ??
+ rawUsage.num_cached_tokens ??
+ 0;
+ const cachedTokens = typeof rawCachedTokens === "number" && Number.isFinite(rawCachedTokens) ? rawCachedTokens : 0;
+ return Math.min(promptTokens, Math.max(0, cachedTokens));
+}
+
+async function consumeChatStream(
+ model: Model<"mistral-conversations">,
+ output: AssistantMessage,
+ stream: AssistantMessageEventStream,
+ mistralStream: AsyncIterable,
+): Promise {
+ let currentBlock: TextContent | ThinkingContent | null = null;
+ const blocks = output.content;
+ const blockIndex = () => blocks.length - 1;
+ const toolBlocksByKey = new Map();
+
+ const finishCurrentBlock = (block?: typeof currentBlock) => {
+ if (!block) return;
+ if (block.type === "text") {
+ stream.push({
+ type: "text_end",
+ contentIndex: blockIndex(),
+ content: block.text,
+ partial: output,
+ });
+ return;
+ }
+ if (block.type === "thinking") {
+ stream.push({
+ type: "thinking_end",
+ contentIndex: blockIndex(),
+ content: block.thinking,
+ partial: output,
+ });
+ }
+ };
+
+ for await (const event of mistralStream) {
+ const chunk = event.data;
+ // Mistral's streamed CompletionChunk carries an id field. Keep the first non-empty one,
+ // mirroring how OpenAI-style streaming exposes a stable response identifier per stream.
+ output.responseId ||= chunk.id;
+
+ if (chunk.usage) {
+ const promptTokens = chunk.usage.promptTokens || 0;
+ const cachedPromptTokens = getMistralCachedPromptTokens(chunk.usage, promptTokens);
+
+ output.usage.input = Math.max(0, promptTokens - cachedPromptTokens);
+ output.usage.output = chunk.usage.completionTokens || 0;
+ output.usage.cacheRead = cachedPromptTokens;
+ output.usage.cacheWrite = 0;
+ output.usage.totalTokens =
+ chunk.usage.totalTokens ||
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
+ calculateCost(model, output.usage);
+ }
+
+ const choice = chunk.choices[0];
+ if (!choice) continue;
+
+ if (choice.finishReason) {
+ output.stopReason = mapChatStopReason(choice.finishReason);
+ }
+
+ const delta = choice.delta;
+ if (delta.content !== null && delta.content !== undefined) {
+ const contentItems = typeof delta.content === "string" ? [delta.content] : delta.content;
+ for (const item of contentItems) {
+ if (typeof item === "string") {
+ const textDelta = sanitizeSurrogates(item);
+ if (!currentBlock || currentBlock.type !== "text") {
+ finishCurrentBlock(currentBlock);
+ currentBlock = { type: "text", text: "" };
+ output.content.push(currentBlock);
+ stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
+ }
+ currentBlock.text += textDelta;
+ stream.push({
+ type: "text_delta",
+ contentIndex: blockIndex(),
+ delta: textDelta,
+ partial: output,
+ });
+ continue;
+ }
+
+ if (item.type === "thinking") {
+ const deltaText = item.thinking
+ .map((part) => ("text" in part ? part.text : ""))
+ .filter((text) => text.length > 0)
+ .join("");
+ const thinkingDelta = sanitizeSurrogates(deltaText);
+ if (!thinkingDelta) continue;
+ if (!currentBlock || currentBlock.type !== "thinking") {
+ finishCurrentBlock(currentBlock);
+ currentBlock = { type: "thinking", thinking: "" };
+ output.content.push(currentBlock);
+ stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
+ }
+ currentBlock.thinking += thinkingDelta;
+ stream.push({
+ type: "thinking_delta",
+ contentIndex: blockIndex(),
+ delta: thinkingDelta,
+ partial: output,
+ });
+ continue;
+ }
+
+ if (item.type === "text") {
+ const textDelta = sanitizeSurrogates(item.text);
+ if (!currentBlock || currentBlock.type !== "text") {
+ finishCurrentBlock(currentBlock);
+ currentBlock = { type: "text", text: "" };
+ output.content.push(currentBlock);
+ stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
+ }
+ currentBlock.text += textDelta;
+ stream.push({
+ type: "text_delta",
+ contentIndex: blockIndex(),
+ delta: textDelta,
+ partial: output,
+ });
+ }
+ }
+ }
+
+ const toolCalls = delta.toolCalls || [];
+ for (const toolCall of toolCalls) {
+ if (currentBlock) {
+ finishCurrentBlock(currentBlock);
+ currentBlock = null;
+ }
+ const callId =
+ toolCall.id && toolCall.id !== "null"
+ ? toolCall.id
+ : deriveMistralToolCallId(`toolcall:${toolCall.index ?? 0}`, 0);
+ const key = `${callId}:${toolCall.index || 0}`;
+ const existingIndex = toolBlocksByKey.get(key);
+ let block: (ToolCall & { partialArgs?: string }) | undefined;
+
+ if (existingIndex !== undefined) {
+ const existing = output.content[existingIndex];
+ if (existing?.type === "toolCall") {
+ block = existing as ToolCall & { partialArgs?: string };
+ }
+ }
+
+ if (!block) {
+ block = {
+ type: "toolCall",
+ id: callId,
+ name: toolCall.function.name,
+ arguments: {},
+ partialArgs: "",
+ };
+ output.content.push(block);
+ toolBlocksByKey.set(key, output.content.length - 1);
+ stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
+ }
+
+ const argsDelta =
+ typeof toolCall.function.arguments === "string"
+ ? toolCall.function.arguments
+ : JSON.stringify(toolCall.function.arguments || {});
+ block.partialArgs = (block.partialArgs || "") + argsDelta;
+ block.arguments = parseStreamingJson>(block.partialArgs);
+ stream.push({
+ type: "toolcall_delta",
+ contentIndex: toolBlocksByKey.get(key)!,
+ delta: argsDelta,
+ partial: output,
+ });
+ }
+ }
+
+ finishCurrentBlock(currentBlock);
+ for (const index of toolBlocksByKey.values()) {
+ const block = output.content[index];
+ if (block.type !== "toolCall") continue;
+ const toolBlock = block as ToolCall & { partialArgs?: string };
+ toolBlock.arguments = parseStreamingJson>(toolBlock.partialArgs);
+ // Finalize in-place and strip the scratch buffer so replay only
+ // carries parsed arguments.
+ delete toolBlock.partialArgs;
+ stream.push({
+ type: "toolcall_end",
+ contentIndex: index,
+ toolCall: toolBlock,
+ partial: output,
+ });
+ }
+}
+
+function toFunctionTools(tools: Tool[]): Array {
+ return tools.map((tool) => ({
+ type: "function",
+ function: {
+ name: tool.name,
+ description: tool.description,
+ parameters: stripSymbolKeys(tool.parameters) as Record,
+ strict: false,
+ },
+ }));
+}
+
+function stripSymbolKeys(value: unknown): unknown {
+ if (Array.isArray(value)) {
+ return value.map((item) => stripSymbolKeys(item));
+ }
+
+ if (value && typeof value === "object") {
+ const result: Record = {};
+ for (const [key, entry] of Object.entries(value)) {
+ result[key] = stripSymbolKeys(entry);
+ }
+ return result;
+ }
+
+ return value;
+}
+
+function toChatMessages(messages: Message[], supportsImages: boolean): ChatCompletionStreamRequestMessage[] {
+ const result: ChatCompletionStreamRequestMessage[] = [];
+
+ for (const msg of messages) {
+ if (msg.role === "user") {
+ if (typeof msg.content === "string") {
+ result.push({ role: "user", content: sanitizeSurrogates(msg.content) });
+ continue;
+ }
+ const hadImages = msg.content.some((item) => item.type === "image");
+ const content: ContentChunk[] = msg.content
+ .filter((item) => item.type === "text" || supportsImages)
+ .map((item) => {
+ if (item.type === "text") return { type: "text", text: sanitizeSurrogates(item.text) };
+ return { type: "image_url", imageUrl: `data:${item.mimeType};base64,${item.data}` };
+ });
+ if (content.length > 0) {
+ result.push({ role: "user", content });
+ continue;
+ }
+ if (hadImages && !supportsImages) {
+ result.push({ role: "user", content: "(image omitted: model does not support images)" });
+ }
+ continue;
+ }
+
+ if (msg.role === "assistant") {
+ const contentParts: ContentChunk[] = [];
+ const toolCalls: Array<{ id: string; type: "function"; function: { name: string; arguments: string } }> = [];
+
+ for (const block of msg.content) {
+ if (block.type === "text") {
+ if (block.text.trim().length > 0) {
+ contentParts.push({ type: "text", text: sanitizeSurrogates(block.text) });
+ }
+ continue;
+ }
+ if (block.type === "thinking") {
+ if (block.thinking.trim().length > 0) {
+ contentParts.push({
+ type: "thinking",
+ thinking: [{ type: "text", text: sanitizeSurrogates(block.thinking) }],
+ });
+ }
+ continue;
+ }
+ toolCalls.push({
+ id: block.id,
+ type: "function",
+ function: { name: block.name, arguments: JSON.stringify(block.arguments || {}) },
+ });
+ }
+
+ const assistantMessage: ChatCompletionStreamRequestMessage = { role: "assistant" };
+ if (contentParts.length > 0) assistantMessage.content = contentParts;
+ if (toolCalls.length > 0) assistantMessage.toolCalls = toolCalls;
+ if (contentParts.length > 0 || toolCalls.length > 0) result.push(assistantMessage);
+ continue;
+ }
+
+ const toolContent: ContentChunk[] = [];
+ const textResult = msg.content
+ .filter((part) => part.type === "text")
+ .map((part) => (part.type === "text" ? sanitizeSurrogates(part.text) : ""))
+ .join("\n");
+ const hasImages = msg.content.some((part) => part.type === "image");
+ const toolText = buildToolResultText(textResult, hasImages, supportsImages, msg.isError);
+ toolContent.push({ type: "text", text: toolText });
+ for (const part of msg.content) {
+ if (!supportsImages) continue;
+ if (part.type !== "image") continue;
+ toolContent.push({
+ type: "image_url",
+ imageUrl: `data:${part.mimeType};base64,${part.data}`,
+ });
+ }
+ result.push({
+ role: "tool",
+ toolCallId: msg.toolCallId,
+ name: msg.toolName,
+ content: toolContent,
+ });
+ }
+
+ return result;
+}
+
+function buildToolResultText(text: string, hasImages: boolean, supportsImages: boolean, isError: boolean): string {
+ const trimmed = text.trim();
+ const errorPrefix = isError ? "[tool error] " : "";
+
+ if (trimmed.length > 0) {
+ const imageSuffix = hasImages && !supportsImages ? "\n[tool image omitted: model does not support images]" : "";
+ return `${errorPrefix}${trimmed}${imageSuffix}`;
+ }
+
+ if (hasImages) {
+ if (supportsImages) {
+ return isError ? "[tool error] (see attached image)" : "(see attached image)";
+ }
+ return isError
+ ? "[tool error] (image omitted: model does not support images)"
+ : "(image omitted: model does not support images)";
+ }
+
+ return isError ? "[tool error] (no tool output)" : "(no tool output)";
+}
+
+function usesReasoningEffort(model: Model<"mistral-conversations">): boolean {
+ return model.id === "mistral-small-2603" || model.id === "mistral-small-latest" || model.id === "mistral-medium-3.5";
+}
+
+function usesPromptModeReasoning(model: Model<"mistral-conversations">): boolean {
+ return model.reasoning && !usesReasoningEffort(model);
+}
+
+function mapReasoningEffort(
+ model: Model<"mistral-conversations">,
+ level: Exclude,
+): MistralReasoningEffort {
+ return (model.thinkingLevelMap?.[level] ?? "high") as MistralReasoningEffort;
+}
+
+function mapToolChoice(
+ choice: MistralOptions["toolChoice"],
+): "auto" | "none" | "any" | "required" | { type: "function"; function: { name: string } } | undefined {
+ if (!choice) return undefined;
+ if (choice === "auto" || choice === "none" || choice === "any" || choice === "required") {
+ return choice as any;
+ }
+ return {
+ type: "function",
+ function: { name: choice.function.name },
+ };
+}
+
+function mapChatStopReason(reason: string | null): StopReason {
+ if (reason === null) return "stop";
+ switch (reason) {
+ case "stop":
+ return "stop";
+ case "length":
+ case "model_length":
+ return "length";
+ case "tool_calls":
+ return "toolUse";
+ case "error":
+ return "error";
+ default:
+ return "stop";
+ }
+}
diff --git a/packages/ai/src/api/openai-codex-responses.lazy.ts b/packages/ai/src/api/openai-codex-responses.lazy.ts
new file mode 100644
index 00000000..a8d0907d
--- /dev/null
+++ b/packages/ai/src/api/openai-codex-responses.lazy.ts
@@ -0,0 +1,4 @@
+import type { ProviderStreams } from "../types.ts";
+import { lazyApi } from "./lazy.ts";
+
+export const openAICodexResponsesApi = (): ProviderStreams => lazyApi(() => import("./openai-codex-responses.ts"));
diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/api/openai-codex-responses.ts
similarity index 93%
rename from packages/ai/src/providers/openai-codex-responses.ts
rename to packages/ai/src/api/openai-codex-responses.ts
index 19a2f5d7..5db519a7 100644
--- a/packages/ai/src/providers/openai-codex-responses.ts
+++ b/packages/ai/src/api/openai-codex-responses.ts
@@ -28,6 +28,7 @@ import type {
Context,
Model,
ProviderEnv,
+ ProviderHeaders,
SimpleStreamOptions,
StreamFunction,
StreamOptions,
@@ -61,6 +62,7 @@ const DEFAULT_SSE_HEADER_TIMEOUT_MS = 20_000;
const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
+const WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE = "websocket_connection_limit_reached";
const CODEX_RESPONSE_STATUSES = new Set([
"completed",
@@ -195,7 +197,7 @@ function createSSEHeaderTimeout(): { signal: AbortSignal; clear: () => void; err
// Main Stream Function
// ============================================================================
-export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions> = (
+export const stream: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions> = (
model: Model<"openai-codex-responses">,
context: Context,
options?: OpenAICodexResponsesOptions,
@@ -253,52 +255,62 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
if (transport !== "sse" && !websocketDisabledForSession) {
let websocketStarted = false;
- try {
- await processWebSocketStream(
- resolveCodexWebSocketUrl(model.baseUrl),
- body,
- websocketHeaders,
- output,
- stream,
- model,
- () => {
- websocketStarted = true;
- },
- idleTimeoutMs,
- websocketConnectTimeoutMs,
- options,
- );
+ let retriedWebSocketConnectionLimit = false;
+ while (true) {
+ websocketStarted = false;
+ try {
+ await processWebSocketStream(
+ resolveCodexWebSocketUrl(model.baseUrl),
+ body,
+ websocketHeaders,
+ output,
+ stream,
+ model,
+ () => {
+ websocketStarted = true;
+ },
+ idleTimeoutMs,
+ websocketConnectTimeoutMs,
+ options,
+ );
- if (options?.signal?.aborted) {
- throw new Error("Request was aborted");
+ if (options?.signal?.aborted) {
+ throw new Error("Request was aborted");
+ }
+ stream.push({
+ type: "done",
+ reason: output.stopReason as "stop" | "length" | "toolUse",
+ message: output,
+ });
+ stream.end();
+ return;
+ } catch (error) {
+ const aborted = options?.signal?.aborted;
+ const connectionLimitBeforeStart = !websocketStarted && isWebSocketConnectionLimitReachedError(error);
+ if (!aborted && connectionLimitBeforeStart && !retriedWebSocketConnectionLimit) {
+ retriedWebSocketConnectionLimit = true;
+ continue;
+ }
+ if (aborted || (isCodexNonTransportError(error) && !connectionLimitBeforeStart)) {
+ throw error;
+ }
+ appendAssistantMessageDiagnostic(
+ output,
+ createAssistantMessageDiagnostic("provider_transport_failure", error, {
+ configuredTransport: transport,
+ fallbackTransport: websocketStarted ? undefined : "sse",
+ eventsEmitted: websocketStarted,
+ phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
+ requestBytes: new TextEncoder().encode(bodyJson).byteLength,
+ }),
+ );
+ recordWebSocketFailure(options?.sessionId, error);
+ if (websocketStarted) {
+ throw error;
+ }
+ recordWebSocketSseFallback(options?.sessionId);
+ break;
}
- stream.push({
- type: "done",
- reason: output.stopReason as "stop" | "length" | "toolUse",
- message: output,
- });
- stream.end();
- return;
- } catch (error) {
- const aborted = options?.signal?.aborted;
- if (aborted || isCodexNonTransportError(error)) {
- throw error;
- }
- appendAssistantMessageDiagnostic(
- output,
- createAssistantMessageDiagnostic("provider_transport_failure", error, {
- configuredTransport: transport,
- fallbackTransport: websocketStarted ? undefined : "sse",
- eventsEmitted: websocketStarted,
- phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
- requestBytes: new TextEncoder().encode(bodyJson).byteLength,
- }),
- );
- recordWebSocketFailure(options?.sessionId, error);
- if (websocketStarted) {
- throw error;
- }
- recordWebSocketSseFallback(options?.sessionId);
}
}
@@ -408,7 +420,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
return stream;
};
-export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-responses", SimpleStreamOptions> = (
+export const streamSimple: StreamFunction<"openai-codex-responses", SimpleStreamOptions> = (
model: Model<"openai-codex-responses">,
context: Context,
options?: SimpleStreamOptions,
@@ -422,7 +434,7 @@ export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-resp
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
- return streamOpenAICodexResponses(model, context, {
+ return stream(model, context, {
...base,
reasoningEffort,
} satisfies OpenAICodexResponsesOptions);
@@ -582,16 +594,32 @@ function isCodexNonTransportError(error: unknown): boolean {
return error instanceof CodexApiError || error instanceof CodexProtocolError;
}
+function isWebSocketConnectionLimitReachedError(error: unknown): boolean {
+ return error instanceof CodexApiError && error.code === WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE;
+}
+
+function extractCodexEventError(event: Record): { code?: string; message?: string } {
+ const nested = event.error && typeof event.error === "object" ? (event.error as Record) : undefined;
+ return {
+ code: typeof event.code === "string" ? event.code : typeof nested?.code === "string" ? nested.code : undefined,
+ message:
+ typeof event.message === "string"
+ ? event.message
+ : typeof nested?.message === "string"
+ ? nested.message
+ : undefined,
+ };
+}
+
async function* mapCodexEvents(events: AsyncIterable>): AsyncGenerator {
for await (const event of events) {
const type = typeof event.type === "string" ? event.type : undefined;
if (!type) continue;
if (type === "error") {
- const code = (event as { code?: string }).code || "";
- const message = (event as { message?: string }).message || "";
+ const { code, message } = extractCodexEventError(event);
throw new CodexApiError(`Codex error: ${message || code || JSON.stringify(event)}`, {
- code: code || undefined,
+ code,
payload: event,
});
}
@@ -1440,13 +1468,17 @@ function createCodexRequestId(): string {
function buildBaseCodexHeaders(
initHeaders: Record | undefined,
- additionalHeaders: Record | undefined,
+ additionalHeaders: ProviderHeaders | undefined,
accountId: string,
token: string,
): Headers {
const headers = new Headers(initHeaders);
for (const [key, value] of Object.entries(additionalHeaders || {})) {
- headers.set(key, value);
+ if (value === null) {
+ headers.delete(key);
+ } else {
+ headers.set(key, value);
+ }
}
headers.set("Authorization", `Bearer ${token}`);
headers.set("chatgpt-account-id", accountId);
@@ -1458,7 +1490,7 @@ function buildBaseCodexHeaders(
function buildSSEHeaders(
initHeaders: Record | undefined,
- additionalHeaders: Record | undefined,
+ additionalHeaders: ProviderHeaders | undefined,
accountId: string,
token: string,
sessionId?: string,
@@ -1478,7 +1510,7 @@ function buildSSEHeaders(
function buildWebSocketHeaders(
initHeaders: Record | undefined,
- additionalHeaders: Record | undefined,
+ additionalHeaders: ProviderHeaders | undefined,
accountId: string,
token: string,
requestId: string,
diff --git a/packages/ai/src/api/openai-completions.lazy.ts b/packages/ai/src/api/openai-completions.lazy.ts
new file mode 100644
index 00000000..6f6c6f61
--- /dev/null
+++ b/packages/ai/src/api/openai-completions.lazy.ts
@@ -0,0 +1,4 @@
+import type { ProviderStreams } from "../types.ts";
+import { lazyApi } from "./lazy.ts";
+
+export const openAICompletionsApi = (): ProviderStreams => lazyApi(() => import("./openai-completions.ts"));
diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/api/openai-completions.ts
similarity index 89%
rename from packages/ai/src/providers/openai-completions.ts
rename to packages/ai/src/api/openai-completions.ts
index 61b03631..e916c52a 100644
--- a/packages/ai/src/providers/openai-completions.ts
+++ b/packages/ai/src/api/openai-completions.ts
@@ -14,12 +14,14 @@ import { calculateCost, clampThinkingLevel } from "../models.ts";
import type {
AssistantMessage,
CacheRetention,
+ ChatTemplateKwargValue,
Context,
ImageContent,
Message,
Model,
OpenAICompletionsCompat,
ProviderEnv,
+ ProviderHeaders,
SimpleStreamOptions,
StopReason,
StreamFunction,
@@ -35,7 +37,6 @@ import { headersToRecord } from "../utils/headers.ts";
import { parseStreamingJson } from "../utils/json-parse.ts";
import { getProviderEnvValue } from "../utils/provider-env.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
-import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { buildBaseOptions } from "./simple-options.ts";
@@ -46,6 +47,21 @@ import { transformMessages } from "./transform-messages.ts";
* This is needed because Anthropic (via proxy) requires the tools param
* to be present when messages include tool_calls or tool role messages.
*/
+function hasHeader(headers: ProviderHeaders | undefined, name: string): boolean {
+ if (!headers) return false;
+ const expected = name.toLowerCase();
+ for (const [key, value] of Object.entries(headers)) {
+ if (key.toLowerCase() === expected && value !== null && value.trim().length > 0) return true;
+ }
+ return false;
+}
+
+function getClientApiKey(provider: string, apiKey: string | undefined, headers: ProviderHeaders | undefined): string {
+ if (apiKey) return apiKey;
+ if (hasHeader(headers, "authorization") || hasHeader(headers, "cf-aig-authorization")) return "unused";
+ throw new Error(`No API key for provider: ${provider}`);
+}
+
function hasToolHistory(messages: Message[]): boolean {
for (const msg of messages) {
if (msg.role === "toolResult") {
@@ -76,6 +92,20 @@ function isImageContentBlock(block: { type: string }): block is ImageContent {
return block.type === "image";
}
+function isEncryptedReasoningDetail(detail: unknown): detail is OpenAIEncryptedReasoningDetail {
+ if (typeof detail !== "object" || detail === null) {
+ return false;
+ }
+ const candidate = detail as Record;
+ return (
+ candidate.type === "reasoning.encrypted" &&
+ typeof candidate.id === "string" &&
+ candidate.id.length > 0 &&
+ typeof candidate.data === "string" &&
+ candidate.data.length > 0
+ );
+}
+
export interface OpenAICompletionsOptions extends StreamOptions {
toolChoice?: "auto" | "none" | "required" | { type: "function"; function: { name: string } };
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
@@ -90,8 +120,16 @@ type ResolvedOpenAICompletionsCompat = Omit, "
cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"];
};
+type ResolvedChatTemplateKwargValue = string | number | boolean | null;
+
type ChatCompletionInstructionMessageParam = ChatCompletionDeveloperMessageParam | ChatCompletionSystemMessageParam;
+type OpenAIEncryptedReasoningDetail = {
+ type: "reasoning.encrypted";
+ id: string;
+ data: string;
+};
+
type ChatCompletionTextPartWithCacheControl = ChatCompletionContentPartText & {
cache_control?: OpenAICompatCacheControl;
};
@@ -110,7 +148,7 @@ function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEn
return "short";
}
-export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenAICompletionsOptions> = (
+export const stream: StreamFunction<"openai-completions", OpenAICompletionsOptions> = (
model: Model<"openai-completions">,
context: Context,
options?: OpenAICompletionsOptions,
@@ -137,14 +175,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
};
try {
- const apiKey = options?.apiKey;
- if (!apiKey) {
- throw new Error(`No API key for provider: ${model.provider}`);
- }
+ const apiKey = getClientApiKey(model.provider, options?.apiKey, options?.headers);
const compat = getCompat(model);
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
- const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat, options?.env);
+ const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat);
let params = buildParams(model, context, options, compat, cacheRetention);
const nextParams = await options?.onPayload?.(params, model);
if (nextParams !== undefined) {
@@ -173,6 +208,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
let hasFinishReason = false;
const toolCallBlocksByIndex = new Map();
const toolCallBlocksById = new Map();
+ const pendingReasoningDetailsByToolCallId = new Map();
const blocks = output.content as StreamingBlock[];
const getContentIndex = (block: StreamingBlock) => blocks.indexOf(block);
const finishBlock = (block: StreamingBlock) => {
@@ -228,6 +264,16 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
}
return thinkingBlock;
};
+ const applyPendingReasoningDetail = (block: StreamingToolCallBlock) => {
+ if (!block.id) {
+ return;
+ }
+ const pendingReasoningDetail = pendingReasoningDetailsByToolCallId.get(block.id);
+ if (pendingReasoningDetail) {
+ block.thoughtSignature = pendingReasoningDetail;
+ pendingReasoningDetailsByToolCallId.delete(block.id);
+ }
+ };
const ensureToolCallBlock = (toolCall: StreamingToolCallDelta) => {
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
let block = streamIndex !== undefined ? toolCallBlocksByIndex.get(streamIndex) : undefined;
@@ -263,6 +309,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
if (toolCall.id) {
toolCallBlocksById.set(toolCall.id, block);
}
+ applyPendingReasoningDetail(block);
return block;
};
@@ -372,15 +419,16 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
}
}
- const reasoningDetails = (choice.delta as any).reasoning_details;
- if (reasoningDetails && Array.isArray(reasoningDetails)) {
+ const reasoningDetails = (choice.delta as { reasoning_details?: unknown }).reasoning_details;
+ if (Array.isArray(reasoningDetails)) {
for (const detail of reasoningDetails) {
- if (detail.type === "reasoning.encrypted" && detail.id && detail.data) {
- const matchingToolCall = output.content.find(
- (b) => b.type === "toolCall" && b.id === detail.id,
- ) as ToolCall | undefined;
+ if (isEncryptedReasoningDetail(detail)) {
+ const serializedDetail = JSON.stringify(detail);
+ const matchingToolCall = toolCallBlocksById.get(detail.id);
if (matchingToolCall) {
- matchingToolCall.thoughtSignature = JSON.stringify(detail);
+ matchingToolCall.thoughtSignature = serializedDetail;
+ } else {
+ pendingReasoningDetailsByToolCallId.set(detail.id, serializedDetail);
}
}
}
@@ -427,22 +475,19 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
return stream;
};
-export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", SimpleStreamOptions> = (
+export const streamSimple: StreamFunction<"openai-completions", SimpleStreamOptions> = (
model: Model<"openai-completions">,
context: Context,
options?: SimpleStreamOptions,
): AssistantMessageEventStream => {
- const apiKey = options?.apiKey;
- if (!apiKey) {
- throw new Error(`No API key for provider: ${model.provider}`);
- }
+ getClientApiKey(model.provider, options?.apiKey, options?.headers);
- const base = buildBaseOptions(model, options, apiKey);
+ const base = buildBaseOptions(model, options, options?.apiKey);
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
const toolChoice = (options as OpenAICompletionsOptions | undefined)?.toolChoice;
- return streamOpenAICompletions(model, context, {
+ return stream(model, context, {
...base,
reasoningEffort,
toolChoice,
@@ -453,12 +498,11 @@ function createClient(
model: Model<"openai-completions">,
context: Context,
apiKey: string,
- optionsHeaders?: Record,
+ optionsHeaders?: ProviderHeaders,
sessionId?: string,
compat: ResolvedOpenAICompletionsCompat = getCompat(model),
- env?: ProviderEnv,
) {
- const headers = { ...model.headers };
+ const headers: ProviderHeaders = { ...model.headers };
if (model.provider === "github-copilot") {
const hasImages = hasCopilotVisionInput(context.messages);
const copilotHeaders = buildCopilotDynamicHeaders({
@@ -479,20 +523,11 @@ function createClient(
Object.assign(headers, optionsHeaders);
}
- const defaultHeaders =
- model.provider === "cloudflare-ai-gateway"
- ? {
- ...headers,
- Authorization: headers.Authorization ?? null,
- "cf-aig-authorization": `Bearer ${apiKey}`,
- }
- : headers;
-
return new OpenAI({
apiKey,
- baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model, env) : model.baseUrl,
+ baseURL: model.baseUrl,
dangerouslyAllowBrowser: true,
- defaultHeaders,
+ defaultHeaders: headers,
});
}
@@ -576,6 +611,11 @@ function buildParams(
enable_thinking: !!options?.reasoningEffort,
preserve_thinking: true,
};
+ } else if (compat.thinkingFormat === "chat-template" && model.reasoning) {
+ const chatTemplateKwargs = buildChatTemplateKwargs(model, options, compat);
+ if (chatTemplateKwargs) {
+ (params as any).chat_template_kwargs = chatTemplateKwargs;
+ }
} else if (compat.thinkingFormat === "deepseek" && model.reasoning) {
if (options?.reasoningEffort) {
(params as any).thinking = { type: "enabled" };
@@ -633,7 +673,7 @@ function buildParams(
}
// Vercel AI Gateway provider routing preferences
- if (model.baseUrl.includes("ai-gateway.vercel.sh") && model.compat?.vercelGatewayRouting) {
+ if (model.compat?.vercelGatewayRouting) {
const routing = model.compat.vercelGatewayRouting;
if (routing.only || routing.order) {
const gatewayOptions: Record = {};
@@ -646,6 +686,44 @@ function buildParams(
return params;
}
+function buildChatTemplateKwargs(
+ model: Model<"openai-completions">,
+ options: OpenAICompletionsOptions | undefined,
+ compat: ResolvedOpenAICompletionsCompat,
+): Record | undefined {
+ const kwargs: Record = {};
+
+ for (const [key, value] of Object.entries(compat.chatTemplateKwargs)) {
+ const resolved = resolveChatTemplateKwargValue(model, options, value);
+ if (resolved !== undefined) {
+ kwargs[key] = resolved;
+ }
+ }
+
+ return Object.keys(kwargs).length > 0 ? kwargs : undefined;
+}
+
+function resolveChatTemplateKwargValue(
+ model: Model<"openai-completions">,
+ options: OpenAICompletionsOptions | undefined,
+ value: ChatTemplateKwargValue,
+): ResolvedChatTemplateKwargValue | undefined {
+ if (typeof value !== "object" || value === null) {
+ return value;
+ }
+
+ const reasoningEffort = options?.reasoningEffort;
+ if (!reasoningEffort && value.omitWhenOff) {
+ return undefined;
+ }
+ if (value.$var === "thinking.enabled") {
+ return !!reasoningEffort;
+ }
+
+ const mappedValue = reasoningEffort ? model.thinkingLevelMap?.[reasoningEffort] : model.thinkingLevelMap?.off;
+ return mappedValue === undefined ? reasoningEffort : typeof mappedValue === "string" ? mappedValue : undefined;
+}
+
function getCompatCacheControl(
compat: ResolvedOpenAICompletionsCompat,
cacheRetention: CacheRetention,
@@ -1086,9 +1164,9 @@ function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"] | str
}
/**
- * Detect compatibility settings from provider and baseUrl for known providers.
- * Provider takes precedence over URL-based detection since it's explicitly configured.
- * Returns a fully resolved OpenAICompletionsCompat object with all fields set.
+ * Auto-detect compatibility settings from provider name and baseUrl.
+ * Used as the base when model.compat is not set; explicit model.compat
+ * entries override these detected values.
*/
function detectCompat(model: Model<"openai-completions">): ResolvedOpenAICompletionsCompat {
const provider = model.provider;
@@ -1158,6 +1236,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
: "openai",
openRouterRouting: {},
vercelGatewayRouting: {},
+ chatTemplateKwargs: {},
zaiToolStream: false,
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia,
cacheControlFormat,
@@ -1174,7 +1253,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
/**
* Get resolved compatibility settings for a model.
- * Uses explicit model.compat if provided, otherwise auto-detects from provider/URL.
+ * Auto-detects from provider/URL then overrides with explicit model.compat.
*/
function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletionsCompat {
const detected = detectCompat(model);
@@ -1196,6 +1275,7 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
openRouterRouting: model.compat.openRouterRouting ?? {},
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
+ chatTemplateKwargs: model.compat.chatTemplateKwargs ?? detected.chatTemplateKwargs,
zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
diff --git a/packages/ai/src/providers/openai-prompt-cache.ts b/packages/ai/src/api/openai-prompt-cache.ts
similarity index 100%
rename from packages/ai/src/providers/openai-prompt-cache.ts
rename to packages/ai/src/api/openai-prompt-cache.ts
diff --git a/packages/ai/src/providers/openai-responses-shared.ts b/packages/ai/src/api/openai-responses-shared.ts
similarity index 91%
rename from packages/ai/src/providers/openai-responses-shared.ts
rename to packages/ai/src/api/openai-responses-shared.ts
index 6fd59a44..72b4b8ae 100644
--- a/packages/ai/src/providers/openai-responses-shared.ts
+++ b/packages/ai/src/api/openai-responses-shared.ts
@@ -294,8 +294,41 @@ export async function processResponsesStream(
): Promise {
let currentItem: ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | null = null;
let currentBlock: ThinkingContent | TextContent | (ToolCall & { partialJson: string }) | null = null;
+ let sawTerminalResponseEvent = false;
const blocks = output.content;
const blockIndex = () => blocks.length - 1;
+ const finalizeResponse = (
+ response: Extract["response"],
+ ): void => {
+ sawTerminalResponseEvent = true;
+ if (response?.id) {
+ output.responseId = response.id;
+ }
+ if (response?.usage) {
+ const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;
+ output.usage = {
+ // OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input
+ input: (response.usage.input_tokens || 0) - cachedTokens,
+ output: response.usage.output_tokens || 0,
+ cacheRead: cachedTokens,
+ cacheWrite: 0,
+ totalTokens: response.usage.total_tokens || 0,
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
+ };
+ }
+ calculateCost(model, output.usage);
+ if (options?.applyServiceTierPricing) {
+ const serviceTier = options.resolveServiceTier
+ ? options.resolveServiceTier(response?.service_tier, options.serviceTier)
+ : (response?.service_tier ?? options.serviceTier);
+ options.applyServiceTierPricing(output.usage, serviceTier);
+ }
+ // Map status to stop reason
+ output.stopReason = mapStopReason(response?.status);
+ if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") {
+ output.stopReason = "toolUse";
+ }
+ };
for await (const event of openaiStream) {
if (event.type === "response.created") {
@@ -491,38 +524,12 @@ export async function processResponsesStream(
currentBlock = null;
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
}
- } else if (event.type === "response.completed") {
- const response = event.response;
- if (response?.id) {
- output.responseId = response.id;
- }
- if (response?.usage) {
- const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;
- output.usage = {
- // OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input
- input: (response.usage.input_tokens || 0) - cachedTokens,
- output: response.usage.output_tokens || 0,
- cacheRead: cachedTokens,
- cacheWrite: 0,
- totalTokens: response.usage.total_tokens || 0,
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
- };
- }
- calculateCost(model, output.usage);
- if (options?.applyServiceTierPricing) {
- const serviceTier = options.resolveServiceTier
- ? options.resolveServiceTier(response?.service_tier, options.serviceTier)
- : (response?.service_tier ?? options.serviceTier);
- options.applyServiceTierPricing(output.usage, serviceTier);
- }
- // Map status to stop reason
- output.stopReason = mapStopReason(response?.status);
- if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") {
- output.stopReason = "toolUse";
- }
+ } else if (event.type === "response.completed" || event.type === "response.incomplete") {
+ finalizeResponse(event.response);
} else if (event.type === "error") {
throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error");
} else if (event.type === "response.failed") {
+ sawTerminalResponseEvent = true;
const error = event.response?.error;
const details = event.response?.incomplete_details;
const msg = error
@@ -533,6 +540,9 @@ export async function processResponsesStream(
throw new Error(msg);
}
}
+ if (!sawTerminalResponseEvent) {
+ throw new Error("OpenAI Responses stream ended before a terminal response event");
+ }
}
function mapStopReason(status: OpenAI.Responses.ResponseStatus | undefined): StopReason {
diff --git a/packages/ai/src/api/openai-responses.lazy.ts b/packages/ai/src/api/openai-responses.lazy.ts
new file mode 100644
index 00000000..066ca801
--- /dev/null
+++ b/packages/ai/src/api/openai-responses.lazy.ts
@@ -0,0 +1,4 @@
+import type { ProviderStreams } from "../types.ts";
+import { lazyApi } from "./lazy.ts";
+
+export const openAIResponsesApi = (): ProviderStreams => lazyApi(() => import("./openai-responses.ts"));
diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/api/openai-responses.ts
similarity index 87%
rename from packages/ai/src/providers/openai-responses.ts
rename to packages/ai/src/api/openai-responses.ts
index 014233d5..40998eaa 100644
--- a/packages/ai/src/providers/openai-responses.ts
+++ b/packages/ai/src/api/openai-responses.ts
@@ -9,6 +9,7 @@ import type {
Model,
OpenAIResponsesCompat,
ProviderEnv,
+ ProviderHeaders,
SimpleStreamOptions,
StreamFunction,
StreamOptions,
@@ -17,7 +18,6 @@ import type {
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { getProviderEnvValue } from "../utils/provider-env.ts";
-import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts";
@@ -25,6 +25,21 @@ import { buildBaseOptions } from "./simple-options.ts";
const OPENAI_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
+function hasHeader(headers: ProviderHeaders | undefined, name: string): boolean {
+ if (!headers) return false;
+ const expected = name.toLowerCase();
+ for (const [key, value] of Object.entries(headers)) {
+ if (key.toLowerCase() === expected && value !== null && value.trim().length > 0) return true;
+ }
+ return false;
+}
+
+function getClientApiKey(provider: string, apiKey: string | undefined, headers: ProviderHeaders | undefined): string {
+ if (apiKey) return apiKey;
+ if (hasHeader(headers, "authorization") || hasHeader(headers, "cf-aig-authorization")) return "unused";
+ throw new Error(`No API key for provider: ${provider}`);
+}
+
/**
* Resolve cache retention preference.
* Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility.
@@ -80,7 +95,7 @@ export interface OpenAIResponsesOptions extends StreamOptions {
/**
* Generate function for OpenAI Responses API
*/
-export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIResponsesOptions> = (
+export const stream: StreamFunction<"openai-responses", OpenAIResponsesOptions> = (
model: Model<"openai-responses">,
context: Context,
options?: OpenAIResponsesOptions,
@@ -109,13 +124,10 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes
try {
// Create OpenAI client
- const apiKey = options?.apiKey;
- if (!apiKey) {
- throw new Error(`No API key for provider: ${model.provider}`);
- }
+ const apiKey = getClientApiKey(model.provider, options?.apiKey, options?.headers);
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
- const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, options?.env);
+ const client = createClient(model, context, apiKey, options?.headers, cacheSessionId);
let params = buildParams(model, context, options);
const nextParams = await options?.onPayload?.(params, model);
if (nextParams !== undefined) {
@@ -161,21 +173,18 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes
return stream;
};
-export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", SimpleStreamOptions> = (
+export const streamSimple: StreamFunction<"openai-responses", SimpleStreamOptions> = (
model: Model<"openai-responses">,
context: Context,
options?: SimpleStreamOptions,
): AssistantMessageEventStream => {
- const apiKey = options?.apiKey;
- if (!apiKey) {
- throw new Error(`No API key for provider: ${model.provider}`);
- }
+ getClientApiKey(model.provider, options?.apiKey, options?.headers);
- const base = buildBaseOptions(model, options, apiKey);
+ const base = buildBaseOptions(model, options, options?.apiKey);
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
- return streamOpenAIResponses(model, context, {
+ return stream(model, context, {
...base,
reasoningEffort,
} satisfies OpenAIResponsesOptions);
@@ -185,12 +194,11 @@ function createClient(
model: Model<"openai-responses">,
context: Context,
apiKey: string,
- optionsHeaders?: Record,
+ optionsHeaders?: ProviderHeaders,
sessionId?: string,
- env?: ProviderEnv,
) {
const compat = getCompat(model);
- const headers = { ...model.headers };
+ const headers: ProviderHeaders = { ...model.headers };
if (model.provider === "github-copilot") {
const hasImages = hasCopilotVisionInput(context.messages);
const copilotHeaders = buildCopilotDynamicHeaders({
@@ -212,20 +220,11 @@ function createClient(
Object.assign(headers, optionsHeaders);
}
- const defaultHeaders =
- model.provider === "cloudflare-ai-gateway"
- ? {
- ...headers,
- Authorization: headers.Authorization ?? null,
- "cf-aig-authorization": `Bearer ${apiKey}`,
- }
- : headers;
-
return new OpenAI({
apiKey,
- baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model, env) : model.baseUrl,
+ baseURL: model.baseUrl,
dangerouslyAllowBrowser: true,
- defaultHeaders,
+ defaultHeaders: headers,
});
}
diff --git a/packages/ai/src/api/openrouter-images.lazy.ts b/packages/ai/src/api/openrouter-images.lazy.ts
new file mode 100644
index 00000000..362d50a0
--- /dev/null
+++ b/packages/ai/src/api/openrouter-images.lazy.ts
@@ -0,0 +1,10 @@
+import type { ImagesModel, ProviderImages } from "../types.ts";
+
+export const openrouterImagesApi = (): ProviderImages => ({
+ generateImages: async (model, context, options) =>
+ (await import("./openrouter-images.ts")).generateImages(
+ model as ImagesModel<"openrouter-images">,
+ context,
+ options,
+ ),
+});
diff --git a/packages/ai/src/providers/images/openrouter.ts b/packages/ai/src/api/openrouter-images.ts
similarity index 93%
rename from packages/ai/src/providers/images/openrouter.ts
rename to packages/ai/src/api/openrouter-images.ts
index 54caeaf0..12117366 100644
--- a/packages/ai/src/providers/images/openrouter.ts
+++ b/packages/ai/src/api/openrouter-images.ts
@@ -13,10 +13,11 @@ import type {
ImagesFunction,
ImagesModel,
ImagesOptions,
+ ProviderHeaders,
TextContent,
-} from "../../types.ts";
-import { headersToRecord } from "../../utils/headers.ts";
-import { sanitizeSurrogates } from "../../utils/sanitize-unicode.ts";
+} from "../types.ts";
+import { headersToRecord, providerHeadersToRecord } from "../utils/headers.ts";
+import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
interface OpenRouterGeneratedImage {
image_url?: string | { url?: string };
@@ -34,7 +35,7 @@ type OpenRouterImageGenerationResponse = ChatCompletion & {
choices: OpenRouterImageGenerationChoice[];
};
-export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", ImagesOptions> = async (
+export const generateImages: ImagesFunction<"openrouter-images", ImagesOptions> = async (
model: ImagesModel<"openrouter-images">,
context: ImagesContext,
options?: ImagesOptions,
@@ -106,16 +107,13 @@ export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", Image
function createClient(
model: ImagesModel<"openrouter-images">,
apiKey: string,
- optionsHeaders?: Record,
+ optionsHeaders?: ProviderHeaders,
): OpenAI {
return new OpenAI({
apiKey,
baseURL: model.baseUrl,
dangerouslyAllowBrowser: true,
- defaultHeaders: {
- ...model.headers,
- ...optionsHeaders,
- },
+ defaultHeaders: providerHeadersToRecord({ ...model.headers, ...optionsHeaders }),
});
}
diff --git a/packages/ai/src/providers/simple-options.ts b/packages/ai/src/api/simple-options.ts
similarity index 100%
rename from packages/ai/src/providers/simple-options.ts
rename to packages/ai/src/api/simple-options.ts
diff --git a/packages/ai/src/providers/transform-messages.ts b/packages/ai/src/api/transform-messages.ts
similarity index 100%
rename from packages/ai/src/providers/transform-messages.ts
rename to packages/ai/src/api/transform-messages.ts
diff --git a/packages/ai/src/auth/context.ts b/packages/ai/src/auth/context.ts
new file mode 100644
index 00000000..30e088bf
--- /dev/null
+++ b/packages/ai/src/auth/context.ts
@@ -0,0 +1,45 @@
+import type { AuthContext } from "./types.ts";
+
+interface NodeFsModule {
+ access(path: string): Promise;
+}
+
+interface NodeOsModule {
+ homedir(): string;
+}
+
+// Variable specifier so browser bundlers do not try to resolve node builtins.
+const importNodeModule = (specifier: string): Promise => import(specifier);
+
+function getProcessEnv(): Record | undefined {
+ const proc = (globalThis as { process?: { env?: Record } }).process;
+ return proc?.env;
+}
+
+/**
+ * Default auth context: env vars from `process.env` (undefined in browsers),
+ * file existence via node:fs (always false in browsers).
+ */
+export function defaultProviderAuthContext(): AuthContext {
+ return {
+ async env(name: string): Promise {
+ const value = getProcessEnv()?.[name];
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
+ },
+
+ async fileExists(path: string): Promise {
+ try {
+ const fs = (await importNodeModule("node:fs/promises")) as NodeFsModule;
+ let resolved = path;
+ if (resolved.startsWith("~")) {
+ const os = (await importNodeModule("node:os")) as NodeOsModule;
+ resolved = os.homedir() + resolved.slice(1);
+ }
+ await fs.access(resolved);
+ return true;
+ } catch {
+ return false;
+ }
+ },
+ };
+}
diff --git a/packages/ai/src/auth/credential-store.ts b/packages/ai/src/auth/credential-store.ts
new file mode 100644
index 00000000..beeb9d85
--- /dev/null
+++ b/packages/ai/src/auth/credential-store.ts
@@ -0,0 +1,47 @@
+import type { Credential, CredentialStore } from "./types.ts";
+
+/**
+ * Default in-memory credential store. Apps inject persistent stores.
+ * Keyed by `Provider.id`, one credential per provider; see `CredentialStore`.
+ * Writes are serialized per provider through a promise chain.
+ */
+export class InMemoryCredentialStore implements CredentialStore {
+ private credentials = new Map();
+ private chains = new Map>();
+
+ /** Serialize tasks per provider id. */
+ private enqueue(providerId: string, task: () => Promise): Promise {
+ const previous = this.chains.get(providerId) ?? Promise.resolve();
+ const next = (async () => {
+ await previous.catch(() => {});
+ return task();
+ })();
+ this.chains.set(
+ providerId,
+ next.catch(() => {}),
+ );
+ return next;
+ }
+
+ async read(providerId: string): Promise {
+ return this.credentials.get(providerId);
+ }
+
+ modify(
+ providerId: string,
+ fn: (current: Credential | undefined) => Promise,
+ ): Promise {
+ return this.enqueue(providerId, async () => {
+ const current = this.credentials.get(providerId);
+ const next = await fn(current);
+ if (next !== undefined) this.credentials.set(providerId, next);
+ return next ?? current;
+ });
+ }
+
+ delete(providerId: string): Promise