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 @@

Discord -

-

- pi.dev domain graciously donated by -

- Exy mascot
exe.dev
+ npm

> 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 +

+ Exy mascot
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 { + return this.enqueue(providerId, async () => { + this.credentials.delete(providerId); + }); + } +} diff --git a/packages/ai/src/auth/helpers.ts b/packages/ai/src/auth/helpers.ts new file mode 100644 index 00000000..d9a34ad2 --- /dev/null +++ b/packages/ai/src/auth/helpers.ts @@ -0,0 +1,46 @@ +import type { ApiKeyAuth, OAuthAuth } from "./types.ts"; + +/** + * Standard api-key auth: a stored credential key wins, otherwise the first + * set env var resolves. Includes a `login` that prompts for the key. + * Providers with non-standard resolution (provider env, ambient files, IAM) + * write their own `ApiKeyAuth`. + */ +export function envApiKeyAuth(name: string, envVars: readonly string[]): ApiKeyAuth { + return { + name, + login: async (callbacks) => { + const key = await callbacks.prompt({ type: "secret", message: `Enter ${name}` }); + return { type: "api_key", key }; + }, + resolve: async ({ ctx, credential }) => { + if (credential?.key) return { auth: { apiKey: credential.key }, source: "stored credential" }; + for (const envVar of envVars) { + const value = await ctx.env(envVar); + if (value) return { auth: { apiKey: value }, source: envVar }; + } + return undefined; + }, + }; +} + +/** + * Wraps a dynamically imported `OAuthAuth` so provider definitions can + * advertise OAuth without importing the implementation. The flow loads on + * first `login`/`refresh`/`toAuth` call; callers keep Node-only flow code out + * of bundles by loading through a bundler-opaque dynamic import (variable + * specifier, see the bedrock lazy wrapper). + */ +export function lazyOAuth(input: { name: string; load: () => Promise }): OAuthAuth { + let promise: Promise | undefined; + const loaded = () => { + promise ??= input.load(); + return promise; + }; + return { + name: input.name, + login: async (callbacks) => (await loaded()).login(callbacks), + refresh: async (credential) => (await loaded()).refresh(credential), + toAuth: async (credential) => (await loaded()).toAuth(credential), + }; +} diff --git a/packages/ai/src/auth/resolve.ts b/packages/ai/src/auth/resolve.ts new file mode 100644 index 00000000..81d7a270 --- /dev/null +++ b/packages/ai/src/auth/resolve.ts @@ -0,0 +1,141 @@ +import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv } from "../types.ts"; +import type { + ApiKeyAuth, + ApiKeyCredential, + AuthContext, + AuthResult, + Credential, + CredentialStore, + OAuthAuth, + OAuthCredential, + ProviderAuth, +} from "./types.ts"; + +export type ModelsErrorCode = "model_source" | "model_validation" | "provider" | "stream" | "auth" | "oauth"; + +export interface AuthResolutionOverrides { + apiKey?: string; + env?: ProviderEnv; +} + +export class ModelsError extends Error { + readonly code: ModelsErrorCode; + + constructor(code: ModelsErrorCode, message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "ModelsError"; + this.code = code; + } +} + +/** Model shape auth resolution receives: chat or image-generation models. */ +export type AuthModel = Model | ImagesModel; + +/** + * Auth resolution shared by the `Models` and `ImagesModels` collections. + * A stored credential owns the provider: ambient/env is consulted only when + * nothing is stored. No silent env fallback after a failed refresh or for a + * credential type without a matching handler. + */ +export async function resolveProviderAuth( + provider: { id: string; auth: ProviderAuth }, + model: AuthModel, + credentials: CredentialStore, + authContext: AuthContext, + overrides?: AuthResolutionOverrides, +): Promise { + const requestAuthContext = overrides?.env ? overlayEnvAuthContext(authContext, overrides.env) : authContext; + + if (overrides?.apiKey !== undefined && provider.auth.apiKey) { + return resolveApiKey(requestAuthContext, provider.auth.apiKey, model, { + type: "api_key", + key: overrides.apiKey, + env: overrides.env, + }); + } + + const stored = await readCredential(credentials, provider.id); + if (stored) { + if (stored.type === "oauth" && provider.auth.oauth) { + return resolveStoredOAuth(credentials, provider.id, provider.auth.oauth, stored); + } + if (stored.type === "api_key" && provider.auth.apiKey) { + const credential = overrides?.env ? { ...stored, env: { ...stored.env, ...overrides.env } } : stored; + return resolveApiKey(requestAuthContext, provider.auth.apiKey, model, credential); + } + return undefined; + } + + // Ambient (env vars, AWS profiles, ADC files). + return provider.auth.apiKey ? resolveApiKey(requestAuthContext, provider.auth.apiKey, model, undefined) : undefined; +} + +function overlayEnvAuthContext(base: AuthContext, env: ProviderEnv): AuthContext { + return { + env: async (name) => env[name] || (await base.env(name)), + fileExists: (path) => base.fileExists(path), + }; +} + +/** + * OAuth resolution with double-checked locking (same pattern as today's + * AuthStorage): valid tokens cost zero locks; expired tokens lock, re-check + * expiry under the lock, refresh once globally, and persist the rotated + * credential before release. + */ +async function resolveStoredOAuth( + credentials: CredentialStore, + providerId: string, + oauth: OAuthAuth, + stored: OAuthCredential, +): Promise { + let credential = stored; + + if (Date.now() >= credential.expires) { + // Optimistic check said expired; the authoritative check runs under the lock. + let post: Credential | undefined; + try { + post = await credentials.modify(providerId, async (current) => { + if (current?.type !== "oauth") return undefined; // logged out meanwhile + if (Date.now() < current.expires) return undefined; // another process/request refreshed + try { + return await oauth.refresh(current); + } catch (error) { + throw new ModelsError("oauth", `OAuth refresh failed for ${providerId}`, { cause: error }); + } + }); + } catch (error) { + if (error instanceof ModelsError) throw error; + throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error }); + } + if (post?.type !== "oauth") return undefined; // logged out meanwhile + credential = post; + } + + try { + return { auth: await oauth.toAuth(credential), source: "OAuth" }; + } catch (error) { + throw new ModelsError("oauth", `OAuth auth derivation failed for ${providerId}`, { cause: error }); + } +} + +async function resolveApiKey( + authContext: AuthContext, + apiKey: ApiKeyAuth, + model: AuthModel, + credential: ApiKeyCredential | undefined, +): Promise { + try { + return await apiKey.resolve({ model, ctx: authContext, credential }); + } catch (error) { + throw new ModelsError("auth", `API key auth failed for provider ${model.provider}`, { cause: error }); + } +} + +async function readCredential(credentials: CredentialStore, providerId: string): Promise { + try { + return await credentials.read(providerId); + } catch (error) { + throw new ModelsError("auth", `Credential store read failed for ${providerId}`, { cause: error }); + } +} diff --git a/packages/ai/src/auth/types.ts b/packages/ai/src/auth/types.ts new file mode 100644 index 00000000..9710dbcb --- /dev/null +++ b/packages/ai/src/auth/types.ts @@ -0,0 +1,182 @@ +import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv, ProviderHeaders } from "../types.ts"; +import type { OAuthCredentials } from "../utils/oauth/types.ts"; + +/** + * Request auth for a single model request. If a value cannot be expressed as + * `apiKey`, `headers`, or `baseUrl`, it is provider config, not auth. + */ +export interface ModelAuth { + apiKey?: string; + headers?: ProviderHeaders; + baseUrl?: string; +} + +/** + * Stored api-key credential. `env` holds provider-scoped environment/config + * values such as Cloudflare account/gateway ids. + */ +export interface ApiKeyCredential { + type: "api_key"; + key?: string; + env?: ProviderEnv; +} + +/** Stored OAuth credential (`access`, `refresh`, `expires` from OAuthCredentials). */ +export interface OAuthCredential extends OAuthCredentials { + type: "oauth"; +} + +/** One type-tagged credential per provider — the shape of today's auth.json. */ +export type Credential = ApiKeyCredential | OAuthCredential; + +/** + * App-owned credential storage, keyed by `Provider.id`, one credential per + * provider. `modify` is the only write path, so every mutation is a + * serialized read-modify-write; `Models.getAuth()` runs OAuth refresh inside + * `modify` so concurrent requests cannot double-refresh a rotated token. The + * app persists a credential after login via + * `modify(provider.id, async () => credential)`. Login/logout orchestration + * is app-owned. + * + * Error semantics: `read` resolves `undefined` for missing entries. Methods + * reject only on storage failure; `Models` wraps such rejections in + * `ModelsError` with code "auth". Best-effort stores that serve an in-memory + * view and record persistence errors internally (like coding-agent's + * AuthStorage) are valid implementations. + */ +export interface CredentialStore { + /** + * Read the stored credential, possibly expired. Display/status use; + * resolved 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 (e.g. a file lock). Resolves with the post-write + * credential. Rejections from `fn` propagate. + */ + modify( + providerId: string, + fn: (current: Credential | undefined) => Promise, + ): Promise; + + /** Remove a credential (logout). Implementations serialize this against `modify`. */ + delete(providerId: string): Promise; +} + +/** Environment access for auth resolution. Injectable for tests and browsers. */ +export interface AuthContext { + env(name: string): Promise; + /** Check whether a file exists. Supports a leading `~`. Always false in browsers. */ + fileExists(path: string): Promise; +} + +/** Result of resolving auth for a model. */ +export interface AuthResult { + auth: ModelAuth; + /** Provider-scoped environment/config values resolved from credentials and ambient context. */ + env?: ProviderEnv; + /** Human-readable label for status UI: "ANTHROPIC_API_KEY", "OAuth", "~/.aws/credentials". */ + source?: string; +} + +/** + * Prompt shown to the user during login. `signal` lets the flow cancel a + * pending prompt when an out-of-band event resolves the step, e.g. a + * `manual_code` prompt raced against a callback server, aborted when the + * callback wins. + */ +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 }; + +/** + * Login interaction callbacks serving both api-key and OAuth flows. + * + * `prompt()` returns the entered/selected string (`select` returns the option + * id). Rejects on cancel/abort. `signal` aborts the whole login flow; + * per-prompt cancellation uses `AuthPrompt.signal`. + */ +export interface AuthLoginCallbacks { + signal?: AbortSignal; + + prompt(prompt: AuthPrompt): Promise; + notify(event: AuthEvent): void; +} + +/** + * Api-key auth: stored key/provider env plus ambient sources (env vars, AWS + * profiles, ADC files). Ambient-only providers omit `login`. + */ +export interface ApiKeyAuth { + /** Display name, e.g. "Anthropic API key". */ + name: string; + + /** Interactive setup (prompt for key/provider env). Absent = ambient-only. */ + 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. Receives the chat or image-generation model + * the request is for (both carry `provider` and `baseUrl`). + */ + resolve(input: { + model: Model | ImagesModel; + ctx: AuthContext; + credential?: ApiKeyCredential; + }): Promise; +} + +/** + * OAuth auth. The `refresh`/`toAuth` split lets `Models` own the locked + * refresh pattern: `refresh` produces a credential, `toAuth` derives request + * auth from whatever credential ends up stored. + */ +export interface OAuthAuth { + /** Display name, e.g. "Anthropic (Claude Pro/Max)". */ + name: string; + + login(callbacks: AuthLoginCallbacks): Promise; + + /** + * Exchange the refresh token. Network call; throws on failure + * (invalid_grant etc.). `Models` runs this under the store lock. + */ + refresh(credential: OAuthCredential): Promise; + + /** + * Side-effect-free derivation of request auth from a valid credential. + * Covers per-credential baseUrl (GitHub Copilot). Async so lazy wrappers + * can load the implementation on first use. + */ + toAuth(credential: OAuthCredential): Promise; +} + +/** + * Provider auth. At least one of `apiKey`/`oauth` must be present: even + * ambient-credential providers and keyless local servers provide `apiKey` + * auth whose `resolve()` reports whether the provider is configured. + */ +export interface ProviderAuth { + apiKey?: ApiKeyAuth; + oauth?: OAuthAuth; +} diff --git a/packages/ai/src/bedrock-provider.ts b/packages/ai/src/bedrock-provider.ts index cf08b33e..10430092 100644 --- a/packages/ai/src/bedrock-provider.ts +++ b/packages/ai/src/bedrock-provider.ts @@ -1,6 +1,6 @@ -import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; +import { stream, streamSimple } from "./api/bedrock-converse-stream.ts"; export const bedrockProviderModule = { - streamBedrock, - streamSimpleBedrock, + stream, + streamSimple, }; diff --git a/packages/ai/src/compat.ts b/packages/ai/src/compat.ts new file mode 100644 index 00000000..91ecda4f --- /dev/null +++ b/packages/ai/src/compat.ts @@ -0,0 +1,277 @@ +/** + * Temporary compatibility entrypoint preserving the old global pi-ai API + * surface: api-dispatch `stream()`/`complete()` with env API key injection, + * the api-registry, generated catalog reads (`getModel`/`getModels`/ + * `getProviders`), per-API lazy stream wrappers, and image generation. + * + * Existing apps switch imports from "@earendil-works/pi-ai" to + * "@earendil-works/pi-ai/compat" unchanged; new code uses `createModels()` + * and the provider factories. This module is deleted with the coding-agent + * ModelManager migration. + */ + +export * from "./api/anthropic-messages.lazy.ts"; +export * from "./api/azure-openai-responses.lazy.ts"; +export * from "./api/bedrock-converse-stream.lazy.ts"; +export * from "./api/google-generative-ai.lazy.ts"; +export * from "./api/google-vertex.lazy.ts"; +export * from "./api/mistral-conversations.lazy.ts"; +export * from "./api/openai-codex-responses.lazy.ts"; +export * from "./api/openai-completions.lazy.ts"; +export * from "./api/openai-responses.lazy.ts"; +export * from "./env-api-keys.ts"; +export * from "./image-models.ts"; +export * from "./images.ts"; +export * from "./images-api-registry.ts"; +export * from "./index.ts"; +export * from "./legacy-api-aliases.ts"; +export * from "./providers/images/register-builtins.ts"; + +import { anthropicMessagesApi } from "./api/anthropic-messages.lazy.ts"; +import { azureOpenAIResponsesApi } from "./api/azure-openai-responses.lazy.ts"; +import { bedrockConverseStreamApi } from "./api/bedrock-converse-stream.lazy.ts"; +import { googleGenerativeAIApi } from "./api/google-generative-ai.lazy.ts"; +import { googleVertexApi } from "./api/google-vertex.lazy.ts"; +import { mistralConversationsApi } from "./api/mistral-conversations.lazy.ts"; +import { openAICodexResponsesApi } from "./api/openai-codex-responses.lazy.ts"; +import { openAICompletionsApi } from "./api/openai-completions.lazy.ts"; +import { openAIResponsesApi } from "./api/openai-responses.lazy.ts"; +import { getEnvApiKey } from "./env-api-keys.ts"; +import { builtinModels, getBuiltinModel, getBuiltinModels, getBuiltinProviders } from "./providers/all.ts"; +import { createFauxCore, type FauxProviderRegistration, type RegisterFauxProviderOptions } from "./providers/faux.ts"; +import type { + Api, + ApiStreamOptions, + AssistantMessage, + AssistantMessageEventStream, + Context, + Model, + ProviderStreamOptions, + ProviderStreams, + SimpleStreamOptions, + StreamFunction, + StreamOptions, +} from "./types.ts"; + +/** @deprecated Static catalog read. Use `getBuiltinModel` from "@earendil-works/pi-ai/providers/all" or `Models.getModel()`. */ +export const getModel = getBuiltinModel; + +/** @deprecated Static catalog read. Use `getBuiltinModels` from "@earendil-works/pi-ai/providers/all" or `Models.getModels()`. */ +export const getModels = getBuiltinModels; + +/** @deprecated Static catalog read. Use `getBuiltinProviders` from "@earendil-works/pi-ai/providers/all" or `Models.getProviders()`. */ +export const getProviders = getBuiltinProviders; + +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); + } + } +} + +function clearApiProviders(): void { + apiProviderRegistry.clear(); +} + +export function registerFauxProvider(options: RegisterFauxProviderOptions = {}): FauxProviderRegistration { + const core = createFauxCore(options); + const sourceId = `faux-provider-${Math.random().toString(36).slice(2, 10)}`; + registerApiProvider({ api: core.api, stream: core.stream, streamSimple: core.streamSimple }, sourceId); + return { + api: core.api, + models: core.models, + getModel: core.getModel, + state: core.state, + setResponses: core.setResponses, + appendResponses: core.appendResponses, + getPendingResponseCount: core.getPendingResponseCount, + unregister() { + unregisterApiProviders(sourceId); + }, + }; +} + +const BUILTIN_APIS: [Api, ProviderStreams][] = [ + ["anthropic-messages", anthropicMessagesApi()], + ["openai-completions", openAICompletionsApi()], + ["openai-responses", openAIResponsesApi()], + ["openai-codex-responses", openAICodexResponsesApi()], + ["azure-openai-responses", azureOpenAIResponsesApi()], + ["google-generative-ai", googleGenerativeAIApi()], + ["google-vertex", googleVertexApi()], + ["mistral-conversations", mistralConversationsApi()], + ["bedrock-converse-stream", bedrockConverseStreamApi()], +]; + +const builtinApiProviderInstances = new Map>(); + +/** + * Registers the builtin API implementations into the api-registry without + * clobbering existing entries: compat may load after a test or extension has + * already registered an override for a builtin api id. + */ +export function registerBuiltInApiProviders(): void { + for (const [api, streams] of BUILTIN_APIS) { + if (!getApiProvider(api)) { + registerApiProvider({ api, stream: streams.stream, streamSimple: streams.streamSimple }); + } + builtinApiProviderInstances.set(api, getApiProvider(api)); + } +} + +export function resetApiProviders(): void { + clearApiProviders(); + builtinApiProviderInstances.clear(); + registerBuiltInApiProviders(); +} + +registerBuiltInApiProviders(); + +const compatModels = builtinModels(); + +function hasExplicitApiKey(apiKey: string | undefined): apiKey is string { + return typeof apiKey === "string" && apiKey.trim().length > 0; +} + +function withEnvApiKey( + model: Model, + options: TOptions | undefined, +): TOptions | undefined { + if (hasExplicitApiKey(options?.apiKey)) return options; + const apiKey = getEnvApiKey(model.provider, options?.env); + if (!apiKey) return options; + return { ...options, apiKey } as TOptions; +} + +function shouldUseBuiltinModels(model: Model): boolean { + const builtin = compatModels.getModel(model.provider, model.id); + return builtin?.api === model.api && getApiProvider(model.api) === builtinApiProviderInstances.get(model.api); +} + +function resolveApiProvider(api: Api) { + const provider = getApiProvider(api); + if (!provider) { + throw new Error(`No API provider registered for api: ${api}`); + } + return provider; +} + +export function stream( + model: Model, + context: Context, + options?: ProviderStreamOptions, +): AssistantMessageEventStream { + if (shouldUseBuiltinModels(model)) { + return compatModels.stream(model, context, options as ApiStreamOptions | undefined); + } + const provider = resolveApiProvider(model.api); + return provider.stream(model, context, withEnvApiKey(model, options) as StreamOptions); +} + +export async function complete( + model: Model, + context: Context, + options?: ProviderStreamOptions, +): Promise { + const s = stream(model, context, options); + return s.result(); +} + +export function streamSimple( + model: Model, + context: Context, + options?: SimpleStreamOptions, +): AssistantMessageEventStream { + if (shouldUseBuiltinModels(model)) { + return compatModels.streamSimple(model, context, options); + } + const provider = resolveApiProvider(model.api); + return provider.streamSimple(model, context, withEnvApiKey(model, options)); +} + +export async function completeSimple( + model: Model, + context: Context, + options?: SimpleStreamOptions, +): Promise { + const s = streamSimple(model, context, options); + return s.result(); +} diff --git a/packages/ai/src/image-models.generated.ts b/packages/ai/src/image-models.generated.ts index 09c74180..7545a9f1 100644 --- a/packages/ai/src/image-models.generated.ts +++ b/packages/ai/src/image-models.generated.ts @@ -95,6 +95,21 @@ export const IMAGE_MODELS = { cacheWrite: 0.08333333333333334, }, } satisfies ImagesModel<"openrouter-images">, + "google/gemini-3-pro-image": { + id: "google/gemini-3-pro-image", + name: "Google: Nano Banana Pro (Gemini 3 Pro Image)", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 2, + output: 12, + cacheRead: 0.19999999999999998, + cacheWrite: 0.375, + }, + } satisfies ImagesModel<"openrouter-images">, "google/gemini-3-pro-image-preview": { id: "google/gemini-3-pro-image-preview", name: "Google: Nano Banana Pro (Gemini 3 Pro Image Preview)", @@ -110,6 +125,21 @@ export const IMAGE_MODELS = { cacheWrite: 0.375, }, } satisfies ImagesModel<"openrouter-images">, + "google/gemini-3.1-flash-image": { + id: "google/gemini-3.1-flash-image", + name: "Google: Nano Banana 2 (Gemini 3.1 Flash Image)", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, "google/gemini-3.1-flash-image-preview": { id: "google/gemini-3.1-flash-image-preview", name: "Google: Nano Banana 2 (Gemini 3.1 Flash Image Preview)", diff --git a/packages/ai/src/images-models.ts b/packages/ai/src/images-models.ts new file mode 100644 index 00000000..0ca5f2da --- /dev/null +++ b/packages/ai/src/images-models.ts @@ -0,0 +1,267 @@ +import { defaultProviderAuthContext as defaultAuthContext } from "./auth/context.ts"; +import { InMemoryCredentialStore } from "./auth/credential-store.ts"; +import { ModelsError, resolveProviderAuth } from "./auth/resolve.ts"; +import type { AuthContext, AuthResult, CredentialStore, ProviderAuth } from "./auth/types.ts"; +import type { CreateModelsOptions } from "./models.ts"; +import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ImagesOptions, ProviderImages } from "./types.ts"; + +/** + * An image-generation provider: the image-side counterpart of `Provider`. + * Owns id/name metadata, auth, model listing, and generation behavior. + */ +export interface ImagesProvider { + readonly id: string; + readonly name: string; + + /** + * Required: at least one of `apiKey`/`oauth`. Same semantics as chat + * providers; `ImagesModels.getAuth()` returns undefined when the provider + * is unconfigured. + */ + readonly auth: ProviderAuth; + + /** + * Current known models, sync. Static providers return their catalog; + * dynamic providers return the list as of the last `refreshModels()` + * (empty before the first). Must not throw; `ImagesModels` treats a + * throwing implementation as having no models. + */ + getModels(): readonly ImagesModel[]; + + /** + * Dynamic providers only: fetch and update the model list. May reject + * (network); on rejection the model list stays at its last-known state + * and a later call retries. + */ + refreshModels?(): Promise; + + generateImages( + model: ImagesModel, + context: ImagesContext, + options?: ImagesOptions, + ): Promise; +} + +/** + * Runtime collection of image-generation providers plus auth application and + * generation convenience: the image-side counterpart of `Models`. + */ +export interface ImagesModels { + getProviders(): readonly ImagesProvider[]; + getProvider(id: string): ImagesProvider | undefined; + + /** + * Sync read of last-known models from one provider or all providers. + * Best-effort: a provider whose `getModels()` throws yields no models. + */ + getModels(provider?: string): readonly ImagesModel[]; + + /** Sync runtime model lookup against last-known lists. */ + getModel(provider: string, id: string): ImagesModel | undefined; + + /** + * Ask dynamic providers to re-fetch their model lists. With a provider id, + * rejects with `ModelsError` ("model_source") on that provider's fetch + * failure; without one, refreshes all providers concurrently best-effort. + * Static providers (no `refreshModels`) are no-ops. + */ + refresh(provider?: string): Promise; + + /** + * Resolve request auth for an image model. Same contract as + * `Models.getAuth()`: undefined when unknown/unconfigured, rejects with + * `ModelsError` ("oauth"/"auth") on real failures. + */ + getAuth(model: ImagesModel): Promise; + + /** + * Generate images through the owning provider with auth resolved and + * merged (explicit options win per field). Never rejects; failures are + * returned as an `AssistantImages` with `stopReason: "error"`. + */ + generateImages( + model: ImagesModel, + context: ImagesContext, + options?: ImagesOptions, + ): Promise; +} + +export interface MutableImagesModels extends ImagesModels { + /** Upsert/replace by provider.id. Provider ids are unique. */ + setProvider(provider: ImagesProvider): void; + deleteProvider(id: string): void; + clearProviders(): void; +} + +class ImagesModelsImpl implements MutableImagesModels { + private providers = new Map(); + private credentials: CredentialStore; + private authContext: AuthContext; + + constructor(options?: CreateModelsOptions) { + this.credentials = options?.credentials ?? new InMemoryCredentialStore(); + this.authContext = options?.authContext ?? defaultAuthContext(); + } + + setProvider(provider: ImagesProvider): void { + this.providers.set(provider.id, provider); + } + + deleteProvider(id: string): void { + this.providers.delete(id); + } + + clearProviders(): void { + this.providers.clear(); + } + + getProviders(): readonly ImagesProvider[] { + return Array.from(this.providers.values()); + } + + getProvider(id: string): ImagesProvider | undefined { + return this.providers.get(id); + } + + getModels(provider?: string): readonly ImagesModel[] { + if (provider !== undefined) { + const entry = this.providers.get(provider); + if (!entry) return []; + try { + return entry.getModels(); + } catch { + return []; + } + } + + const models: ImagesModel[] = []; + for (const entry of this.providers.values()) { + try { + models.push(...entry.getModels()); + } catch { + // Best-effort: ill-behaved providers yield no models. + } + } + return models; + } + + getModel(provider: string, id: string): ImagesModel | undefined { + return this.getModels(provider).find((model) => model.id === id); + } + + async refresh(provider?: string): Promise { + if (provider !== undefined) { + const entry = this.providers.get(provider); + if (!entry?.refreshModels) return; + try { + await entry.refreshModels(); + } catch (error) { + if (error instanceof ModelsError) throw error; + throw new ModelsError("model_source", `Model refresh failed for ${provider}`, { cause: error }); + } + return; + } + + // Cannot reject: the async mapper turns even sync throws from ill-behaved + // providers into rejections, and allSettled captures all of them. + await Promise.allSettled(Array.from(this.providers.values(), async (entry) => entry.refreshModels?.())); + } + + async getAuth(model: ImagesModel): Promise { + const provider = this.providers.get(model.provider); + if (!provider) return undefined; + return resolveProviderAuth(provider, model, this.credentials, this.authContext); + } + + async generateImages( + model: ImagesModel, + context: ImagesContext, + options?: ImagesOptions, + ): Promise { + try { + const provider = this.providers.get(model.provider); + if (!provider) { + throw new ModelsError("provider", `Unknown provider: ${model.provider}`); + } + + const resolution = await resolveProviderAuth(provider, model, this.credentials, this.authContext, { + apiKey: options?.apiKey, + env: options?.env, + }); + const auth = resolution?.auth; + if (!auth) { + return provider.generateImages(model, context, options); + } + + const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model; + + // Explicit request options win per-field; headers/env merge per key. + const apiKey = options?.apiKey ?? auth.apiKey; + const headers = auth.headers || options?.headers ? { ...auth.headers, ...options?.headers } : undefined; + const env = + resolution.env || options?.env ? { ...(resolution.env ?? {}), ...(options?.env ?? {}) } : undefined; + + return await provider.generateImages(requestModel, context, { ...options, apiKey, headers, env }); + } catch (error) { + return { + api: model.api, + provider: model.provider, + model: model.id, + output: [], + stopReason: "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + }; + } + } +} + +export function createImagesModels(options?: CreateModelsOptions): MutableImagesModels { + return new ImagesModelsImpl(options); +} + +export interface CreateImagesProviderOptions { + id: string; + /** Display name. Default: `id`. */ + name?: string; + /** Required — every provider has auth semantics, even ambient/keyless ones. */ + auth: ProviderAuth; + /** Initial model list (empty for purely dynamic providers). */ + models: readonly ImagesModel[]; + /** + * Dynamic providers: fetch the current list. Stored on success; concurrent + * calls share one in-flight fetch. May reject: the stored list then stays + * at its last-known state, the rejection propagates to the caller of + * `refreshModels()` (wrapped as ModelsError "model_source" by + * `ImagesModels.refresh(provider)`), and a later call retries. + */ + refreshModels?: () => Promise[]>; + api: ProviderImages; +} + +/** Builds an image-generation provider from parts. */ +export function createImagesProvider(input: CreateImagesProviderOptions): ImagesProvider { + let models = input.models; + let inflightRefresh: Promise | undefined; + const refreshModels = input.refreshModels; + + return { + id: input.id, + name: input.name ?? input.id, + auth: input.auth, + getModels: () => models, + refreshModels: refreshModels + ? () => { + inflightRefresh ??= (async () => { + try { + models = await refreshModels(); + } finally { + inflightRefresh = undefined; + } + })(); + return inflightRefresh; + } + : undefined, + generateImages: (model, context, options) => input.api.generateImages(model, context, options), + }; +} diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index ed7aeaa8..57c448cc 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,30 +1,30 @@ export type { Static, TSchema } from "typebox"; export { Type } from "typebox"; -export * from "./api-registry.ts"; -export * from "./env-api-keys.ts"; -export * from "./image-models.ts"; -export * from "./images.ts"; -export * from "./images-api-registry.ts"; +// Core only, side-effect free: no generated catalogs, no provider factories, +// no api-registry, no OAuth implementations, no compat. Provider factories +// live under "@earendil-works/pi-ai/providers/*", API implementations under +// "@earendil-works/pi-ai/api/*", the old global API under +// "@earendil-works/pi-ai/compat". +export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./api/anthropic-messages.ts"; +export type { AzureOpenAIResponsesOptions } from "./api/azure-openai-responses.ts"; +export type { BedrockOptions, BedrockThinkingDisplay } from "./api/bedrock-converse-stream.ts"; +export type { GoogleOptions } from "./api/google-generative-ai.ts"; +export type { GoogleThinkingLevel } from "./api/google-shared.ts"; +export type { GoogleVertexOptions } from "./api/google-vertex.ts"; +export * from "./api/lazy.ts"; +export type { MistralOptions } from "./api/mistral-conversations.ts"; +export type { OpenAICodexResponsesOptions, OpenAICodexWebSocketDebugStats } from "./api/openai-codex-responses.ts"; +export type { OpenAICompletionsOptions } from "./api/openai-completions.ts"; +export type { OpenAIResponsesOptions } from "./api/openai-responses.ts"; +export * from "./auth/context.ts"; +export * from "./auth/credential-store.ts"; +export * from "./auth/helpers.ts"; +export * from "./auth/types.ts"; +export * from "./images-models.ts"; export * from "./models.ts"; -export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.ts"; -export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.ts"; -export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.ts"; export * from "./providers/faux.ts"; -export type { GoogleOptions } from "./providers/google.ts"; -export type { GoogleThinkingLevel } from "./providers/google-shared.ts"; -export type { GoogleVertexOptions } from "./providers/google-vertex.ts"; -export * from "./providers/images/register-builtins.ts"; -export type { MistralOptions } from "./providers/mistral.ts"; -export type { - OpenAICodexResponsesOptions, - OpenAICodexWebSocketDebugStats, -} from "./providers/openai-codex-responses.ts"; -export type { OpenAICompletionsOptions } from "./providers/openai-completions.ts"; -export type { OpenAIResponsesOptions } from "./providers/openai-responses.ts"; -export * from "./providers/register-builtins.ts"; export * from "./session-resources.ts"; -export * from "./stream.ts"; export * from "./types.ts"; export * from "./utils/diagnostics.ts"; export * from "./utils/event-stream.ts"; @@ -43,5 +43,6 @@ export type { OAuthSelectPrompt, } from "./utils/oauth/types.ts"; export * from "./utils/overflow.ts"; +export * from "./utils/retry.ts"; export * from "./utils/typebox-helpers.ts"; export * from "./utils/validation.ts"; diff --git a/packages/ai/src/legacy-api-aliases.ts b/packages/ai/src/legacy-api-aliases.ts new file mode 100644 index 00000000..b49c199c --- /dev/null +++ b/packages/ai/src/legacy-api-aliases.ts @@ -0,0 +1,108 @@ +import { anthropicMessagesApi } from "./api/anthropic-messages.lazy.ts"; +import type { AnthropicOptions } from "./api/anthropic-messages.ts"; +import { azureOpenAIResponsesApi } from "./api/azure-openai-responses.lazy.ts"; +import type { AzureOpenAIResponsesOptions } from "./api/azure-openai-responses.ts"; +import { googleGenerativeAIApi } from "./api/google-generative-ai.lazy.ts"; +import type { GoogleOptions } from "./api/google-generative-ai.ts"; +import { googleVertexApi } from "./api/google-vertex.lazy.ts"; +import type { GoogleVertexOptions } from "./api/google-vertex.ts"; +import { mistralConversationsApi } from "./api/mistral-conversations.lazy.ts"; +import type { MistralOptions } from "./api/mistral-conversations.ts"; +import { openAICodexResponsesApi } from "./api/openai-codex-responses.lazy.ts"; +import type { OpenAICodexResponsesOptions } from "./api/openai-codex-responses.ts"; +import { openAICompletionsApi } from "./api/openai-completions.lazy.ts"; +import type { OpenAICompletionsOptions } from "./api/openai-completions.ts"; +import { openAIResponsesApi } from "./api/openai-responses.lazy.ts"; +import type { OpenAIResponsesOptions } from "./api/openai-responses.ts"; +import type { SimpleStreamOptions, StreamFunction } from "./types.ts"; + +const anthropicMessagesStreams = anthropicMessagesApi(); +const azureOpenAIResponsesStreams = azureOpenAIResponsesApi(); +const googleGenerativeAIStreams = googleGenerativeAIApi(); +const googleVertexStreams = googleVertexApi(); +const mistralConversationsStreams = mistralConversationsApi(); +const openAICodexResponsesStreams = openAICodexResponsesApi(); +const openAICompletionsStreams = openAICompletionsApi(); +const openAIResponsesStreams = openAIResponsesApi(); + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/anthropic-messages` or `anthropicMessagesApi().stream`. */ +export const streamAnthropic = anthropicMessagesStreams.stream as StreamFunction< + "anthropic-messages", + AnthropicOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/anthropic-messages` or `anthropicMessagesApi().streamSimple`. */ +export const streamSimpleAnthropic = anthropicMessagesStreams.streamSimple as StreamFunction< + "anthropic-messages", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/azure-openai-responses` or `azureOpenAIResponsesApi().stream`. */ +export const streamAzureOpenAIResponses = azureOpenAIResponsesStreams.stream as StreamFunction< + "azure-openai-responses", + AzureOpenAIResponsesOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/azure-openai-responses` or `azureOpenAIResponsesApi().streamSimple`. */ +export const streamSimpleAzureOpenAIResponses = azureOpenAIResponsesStreams.streamSimple as StreamFunction< + "azure-openai-responses", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/google-generative-ai` or `googleGenerativeAIApi().stream`. */ +export const streamGoogle = googleGenerativeAIStreams.stream as StreamFunction<"google-generative-ai", GoogleOptions>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/google-generative-ai` or `googleGenerativeAIApi().streamSimple`. */ +export const streamSimpleGoogle = googleGenerativeAIStreams.streamSimple as StreamFunction< + "google-generative-ai", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/google-vertex` or `googleVertexApi().stream`. */ +export const streamGoogleVertex = googleVertexStreams.stream as StreamFunction<"google-vertex", GoogleVertexOptions>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/google-vertex` or `googleVertexApi().streamSimple`. */ +export const streamSimpleGoogleVertex = googleVertexStreams.streamSimple as StreamFunction< + "google-vertex", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/mistral-conversations` or `mistralConversationsApi().stream`. */ +export const streamMistral = mistralConversationsStreams.stream as StreamFunction< + "mistral-conversations", + MistralOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/mistral-conversations` or `mistralConversationsApi().streamSimple`. */ +export const streamSimpleMistral = mistralConversationsStreams.streamSimple as StreamFunction< + "mistral-conversations", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/openai-codex-responses` or `openAICodexResponsesApi().stream`. */ +export const streamOpenAICodexResponses = openAICodexResponsesStreams.stream as StreamFunction< + "openai-codex-responses", + OpenAICodexResponsesOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/openai-codex-responses` or `openAICodexResponsesApi().streamSimple`. */ +export const streamSimpleOpenAICodexResponses = openAICodexResponsesStreams.streamSimple as StreamFunction< + "openai-codex-responses", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/openai-completions` or `openAICompletionsApi().stream`. */ +export const streamOpenAICompletions = openAICompletionsStreams.stream as StreamFunction< + "openai-completions", + OpenAICompletionsOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/openai-completions` or `openAICompletionsApi().streamSimple`. */ +export const streamSimpleOpenAICompletions = openAICompletionsStreams.streamSimple as StreamFunction< + "openai-completions", + SimpleStreamOptions +>; + +/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/openai-responses` or `openAIResponsesApi().stream`. */ +export const streamOpenAIResponses = openAIResponsesStreams.stream as StreamFunction< + "openai-responses", + OpenAIResponsesOptions +>; +/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/openai-responses` or `openAIResponsesApi().streamSimple`. */ +export const streamSimpleOpenAIResponses = openAIResponsesStreams.streamSimple as StreamFunction< + "openai-responses", + SimpleStreamOptions +>; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 424f8bcd..0129ddee 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -1,17181 +1,76 @@ // 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"; +import { AMAZON_BEDROCK_MODELS } from "./providers/amazon-bedrock.models.ts"; +import { ANT_LING_MODELS } from "./providers/ant-ling.models.ts"; +import { ANTHROPIC_MODELS } from "./providers/anthropic.models.ts"; +import { AZURE_OPENAI_RESPONSES_MODELS } from "./providers/azure-openai-responses.models.ts"; +import { CEREBRAS_MODELS } from "./providers/cerebras.models.ts"; +import { CLOUDFLARE_AI_GATEWAY_MODELS } from "./providers/cloudflare-ai-gateway.models.ts"; +import { CLOUDFLARE_WORKERS_AI_MODELS } from "./providers/cloudflare-workers-ai.models.ts"; +import { DEEPSEEK_MODELS } from "./providers/deepseek.models.ts"; +import { FIREWORKS_MODELS } from "./providers/fireworks.models.ts"; +import { GITHUB_COPILOT_MODELS } from "./providers/github-copilot.models.ts"; +import { GOOGLE_MODELS } from "./providers/google.models.ts"; +import { GOOGLE_VERTEX_MODELS } from "./providers/google-vertex.models.ts"; +import { GROQ_MODELS } from "./providers/groq.models.ts"; +import { HUGGINGFACE_MODELS } from "./providers/huggingface.models.ts"; +import { KIMI_CODING_MODELS } from "./providers/kimi-coding.models.ts"; +import { MINIMAX_MODELS } from "./providers/minimax.models.ts"; +import { MINIMAX_CN_MODELS } from "./providers/minimax-cn.models.ts"; +import { MISTRAL_MODELS } from "./providers/mistral.models.ts"; +import { MOONSHOTAI_MODELS } from "./providers/moonshotai.models.ts"; +import { MOONSHOTAI_CN_MODELS } from "./providers/moonshotai-cn.models.ts"; +import { NVIDIA_MODELS } from "./providers/nvidia.models.ts"; +import { OPENAI_MODELS } from "./providers/openai.models.ts"; +import { OPENAI_CODEX_MODELS } from "./providers/openai-codex.models.ts"; +import { OPENCODE_MODELS } from "./providers/opencode.models.ts"; +import { OPENCODE_GO_MODELS } from "./providers/opencode-go.models.ts"; +import { OPENROUTER_MODELS } from "./providers/openrouter.models.ts"; +import { TOGETHER_MODELS } from "./providers/together.models.ts"; +import { VERCEL_AI_GATEWAY_MODELS } from "./providers/vercel-ai-gateway.models.ts"; +import { XAI_MODELS } from "./providers/xai.models.ts"; +import { XIAOMI_MODELS } from "./providers/xiaomi.models.ts"; +import { XIAOMI_TOKEN_PLAN_AMS_MODELS } from "./providers/xiaomi-token-plan-ams.models.ts"; +import { XIAOMI_TOKEN_PLAN_CN_MODELS } from "./providers/xiaomi-token-plan-cn.models.ts"; +import { XIAOMI_TOKEN_PLAN_SGP_MODELS } from "./providers/xiaomi-token-plan-sgp.models.ts"; +import { ZAI_MODELS } from "./providers/zai.models.ts"; +import { ZAI_CODING_CN_MODELS } from "./providers/zai-coding-cn.models.ts"; export const MODELS = { - "amazon-bedrock": { - "amazon.nova-2-lite-v1:0": { - id: "amazon.nova-2-lite-v1:0", - name: "Nova 2 Lite", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.33, - output: 2.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-lite-v1:0": { - id: "amazon.nova-lite-v1:0", - name: "Nova Lite", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.06, - output: 0.24, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-micro-v1:0": { - id: "amazon.nova-micro-v1:0", - name: "Nova Micro", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.035, - output: 0.14, - cacheRead: 0.00875, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-pro-v1:0": { - id: "amazon.nova-pro-v1:0", - name: "Nova Pro", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-1-20250805-v1:0": { - id: "anthropic.claude-opus-4-1-20250805-v1:0", - name: "Claude Opus 4.1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-5-20251101-v1:0": { - id: "anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-6-v1": { - id: "anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-7": { - id: "anthropic.claude-opus-4-7", - name: "Claude Opus 4.7", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-8": { - id: "anthropic.claude-opus-4-8", - name: "Claude Opus 4.8", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-sonnet-4-6": { - id: "anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "au.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "au.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (AU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "au.anthropic.claude-opus-4-6-v1": { - id: "au.anthropic.claude-opus-4-6-v1", - name: "AU Anthropic Claude Opus 4.6", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 16.5, - output: 82.5, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "au.anthropic.claude-opus-4-8": { - id: "au.anthropic.claude-opus-4-8", - name: "Claude Opus 4.8 (AU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "au.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (AU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "au.anthropic.claude-sonnet-4-6": { - id: "au.anthropic.claude-sonnet-4-6", - name: "AU Anthropic Claude Sonnet 4.6", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3.3, - output: 16.5, - cacheRead: 0.33, - cacheWrite: 4.125, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "deepseek.r1-v1:0": { - id: "deepseek.r1-v1:0", - name: "DeepSeek-R1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 1.35, - output: 5.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32768, - } satisfies Model<"bedrock-converse-stream">, - "deepseek.v3-v1:0": { - id: "deepseek.v3-v1:0", - name: "DeepSeek-V3.1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.58, - output: 1.68, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 81920, - } satisfies Model<"bedrock-converse-stream">, - "deepseek.v3.2": { - id: "deepseek.v3.2", - name: "DeepSeek-V3.2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.62, - output: 1.85, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 81920, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-fable-5": { - id: "eu.anthropic.claude-fable-5", - name: "Claude Fable 5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 11, - output: 55, - cacheRead: 1.1, - cacheWrite: 13.75, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "eu.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-6-v1": { - id: "eu.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5.5, - output: 27.5, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-7": { - id: "eu.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5.5, - output: 27.5, - cacheRead: 0.55, - cacheWrite: 6.875, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-8": { - id: "eu.anthropic.claude-opus-4-8", - name: "Claude Opus 4.8 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5.5, - output: 27.5, - cacheRead: 0.55, - cacheWrite: 6.875, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3.3, - output: 16.5, - cacheRead: 0.33, - cacheWrite: 4.125, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-sonnet-4-6": { - id: "eu.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3.3, - output: 16.5, - cacheRead: 0.33, - cacheWrite: 4.125, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-fable-5": { - id: "global.anthropic.claude-fable-5", - name: "Claude Fable 5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "global.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "global.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-6-v1": { - id: "global.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-7": { - id: "global.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-8": { - id: "global.anthropic.claude-opus-4-8", - name: "Claude Opus 4.8 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-sonnet-4-6": { - id: "global.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "google.gemma-3-27b-it": { - id: "google.gemma-3-27b-it", - name: "Google Gemma 3 27B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.12, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "google.gemma-3-4b-it": { - id: "google.gemma-3-4b-it", - name: "Gemma 3 4B IT", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.04, - output: 0.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "jp.anthropic.claude-opus-4-7": { - id: "jp.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (JP)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "jp.anthropic.claude-opus-4-8": { - id: "jp.anthropic.claude-opus-4-8", - name: "Claude Opus 4.8 (JP)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (JP)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "jp.anthropic.claude-sonnet-4-6": { - id: "jp.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (JP)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-1-70b-instruct-v1:0": { - id: "meta.llama3-1-70b-instruct-v1:0", - name: "Llama 3.1 70B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-1-8b-instruct-v1:0": { - id: "meta.llama3-1-8b-instruct-v1:0", - name: "Llama 3.1 8B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 0.22, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-3-70b-instruct-v1:0": { - id: "meta.llama3-3-70b-instruct-v1:0", - name: "Llama 3.3 70B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama4-maverick-17b-instruct-v1:0": { - id: "meta.llama4-maverick-17b-instruct-v1:0", - name: "Llama 4 Maverick 17B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.24, - output: 0.97, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama4-scout-17b-instruct-v1:0": { - id: "meta.llama4-scout-17b-instruct-v1:0", - name: "Llama 4 Scout 17B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.17, - output: 0.66, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 3500000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "minimax.minimax-m2": { - id: "minimax.minimax-m2", - name: "MiniMax M2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204608, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "minimax.minimax-m2.1": { - id: "minimax.minimax-m2.1", - name: "MiniMax M2.1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "minimax.minimax-m2.5": { - id: "minimax.minimax-m2.5", - name: "MiniMax M2.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 98304, - } satisfies Model<"bedrock-converse-stream">, - "mistral.devstral-2-123b": { - id: "mistral.devstral-2-123b", - name: "Devstral 2 123B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.magistral-small-2509": { - id: "mistral.magistral-small-2509", - name: "Magistral Small 1.2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 40000, - } satisfies Model<"bedrock-converse-stream">, - "mistral.ministral-3-14b-instruct": { - id: "mistral.ministral-3-14b-instruct", - name: "Ministral 14B 3.0", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "mistral.ministral-3-3b-instruct": { - id: "mistral.ministral-3-3b-instruct", - name: "Ministral 3 3B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.ministral-3-8b-instruct": { - id: "mistral.ministral-3-8b-instruct", - name: "Ministral 3 8B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "mistral.mistral-large-3-675b-instruct": { - id: "mistral.mistral-large-3-675b-instruct", - name: "Mistral Large 3", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.pixtral-large-2502-v1:0": { - id: "mistral.pixtral-large-2502-v1:0", - name: "Pixtral Large (25.02)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.voxtral-mini-3b-2507": { - id: "mistral.voxtral-mini-3b-2507", - name: "Voxtral Mini 3B 2507", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "mistral.voxtral-small-24b-2507": { - id: "mistral.voxtral-small-24b-2507", - name: "Voxtral Small 24B 2507", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.35, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "moonshot.kimi-k2-thinking": { - id: "moonshot.kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262143, - maxTokens: 16000, - } satisfies Model<"bedrock-converse-stream">, - "moonshotai.kimi-k2.5": { - id: "moonshotai.kimi-k2.5", - name: "Kimi K2.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262143, - maxTokens: 16000, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-nano-12b-v2": { - id: "nvidia.nemotron-nano-12b-v2", - name: "NVIDIA Nemotron Nano 12B v2 VL BF16", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-nano-3-30b": { - id: "nvidia.nemotron-nano-3-30b", - name: "NVIDIA Nemotron Nano 3 30B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-nano-9b-v2": { - id: "nvidia.nemotron-nano-9b-v2", - name: "NVIDIA Nemotron Nano 9B v2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.06, - output: 0.23, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-super-3-120b": { - id: "nvidia.nemotron-super-3-120b", - name: "NVIDIA Nemotron 3 Super 120B A12B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.65, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-5.4": { - id: "openai.gpt-5.4", - name: "GPT-5.4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.75, - output: 16.5, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-5.5": { - id: "openai.gpt-5.5", - name: "GPT-5.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5.5, - output: 33, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-120b": { - id: "openai.gpt-oss-120b", - name: "gpt-oss-120b", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-120b-1:0": { - id: "openai.gpt-oss-120b-1:0", - name: "gpt-oss-120b", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-20b": { - id: "openai.gpt-oss-20b", - name: "gpt-oss-20b", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-20b-1:0": { - id: "openai.gpt-oss-20b-1:0", - name: "gpt-oss-20b", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-safeguard-120b": { - id: "openai.gpt-oss-safeguard-120b", - name: "GPT OSS Safeguard 120B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-safeguard-20b": { - id: "openai.gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.07, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-235b-a22b-2507-v1:0": { - id: "qwen.qwen3-235b-a22b-2507-v1:0", - name: "Qwen3 235B A22B 2507", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 0.88, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-32b-v1:0": { - id: "qwen.qwen3-32b-v1:0", - name: "Qwen3 32B (dense)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16384, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-coder-30b-a3b-v1:0": { - id: "qwen.qwen3-coder-30b-a3b-v1:0", - name: "Qwen3 Coder 30B A3B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-coder-480b-a35b-v1:0": { - id: "qwen.qwen3-coder-480b-a35b-v1:0", - name: "Qwen3 Coder 480B A35B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 1.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-coder-next": { - id: "qwen.qwen3-coder-next", - name: "Qwen3 Coder Next", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.22, - output: 1.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-next-80b-a3b": { - id: "qwen.qwen3-next-80b-a3b", - name: "Qwen/Qwen3-Next-80B-A3B-Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.14, - output: 1.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-vl-235b-a22b": { - id: "qwen.qwen3-vl-235b-a22b", - name: "Qwen/Qwen3-VL-235B-A22B-Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-fable-5": { - id: "us.anthropic.claude-fable-5", - name: "Claude Fable 5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-1-20250805-v1:0": { - id: "us.anthropic.claude-opus-4-1-20250805-v1:0", - name: "Claude Opus 4.1 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "us.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-6-v1": { - id: "us.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-7": { - id: "us.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-8": { - id: "us.anthropic.claude-opus-4-8", - name: "Claude Opus 4.8 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-sonnet-4-6": { - id: "us.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.deepseek.r1-v1:0": { - id: "us.deepseek.r1-v1:0", - name: "DeepSeek-R1 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 1.35, - output: 5.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32768, - } satisfies Model<"bedrock-converse-stream">, - "us.meta.llama4-maverick-17b-instruct-v1:0": { - id: "us.meta.llama4-maverick-17b-instruct-v1:0", - name: "Llama 4 Maverick 17B Instruct (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.24, - output: 0.97, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "us.meta.llama4-scout-17b-instruct-v1:0": { - id: "us.meta.llama4-scout-17b-instruct-v1:0", - name: "Llama 4 Scout 17B Instruct (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.17, - output: 0.66, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 3500000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "writer.palmyra-x4-v1:0": { - id: "writer.palmyra-x4-v1:0", - name: "Palmyra X4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 122880, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "writer.palmyra-x5-v1:0": { - id: "writer.palmyra-x5-v1:0", - name: "Palmyra X5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1040000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "zai.glm-4.7": { - id: "zai.glm-4.7", - name: "GLM-4.7", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "zai.glm-4.7-flash": { - id: "zai.glm-4.7-flash", - name: "GLM-4.7-Flash", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "zai.glm-5": { - id: "zai.glm-5", - name: "GLM-5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 101376, - } satisfies Model<"bedrock-converse-stream">, - }, - "ant-ling": { - "Ling-2.6-1T": { - id: "Ling-2.6-1T", - name: "Ling 2.6 1T", - api: "openai-completions", - provider: "ant-ling", - baseUrl: "https://api.ant-ling.com/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.06, - output: 0.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "Ling-2.6-flash": { - id: "Ling-2.6-flash", - name: "Ling 2.6 Flash", - api: "openai-completions", - provider: "ant-ling", - baseUrl: "https://api.ant-ling.com/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.01, - output: 0.02, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "Ring-2.6-1T": { - id: "Ring-2.6-1T", - name: "Ring 2.6 1T", - api: "openai-completions", - provider: "ant-ling", - baseUrl: "https://api.ant-ling.com/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"thinkingFormat":"ant-ling"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, - input: ["text"], - cost: { - input: 0.06, - output: 0.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - }, - "anthropic": { - "claude-3-5-haiku-20241022": { - id: "claude-3-5-haiku-20241022", - name: "Claude Haiku 3.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-5-haiku-latest": { - id: "claude-3-5-haiku-latest", - name: "Claude Haiku 3.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-5-sonnet-20240620": { - id: "claude-3-5-sonnet-20240620", - name: "Claude Sonnet 3.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-5-sonnet-20241022": { - id: "claude-3-5-sonnet-20241022", - name: "Claude Sonnet 3.5 v2", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude Sonnet 3.7", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-3-haiku-20240307": { - id: "claude-3-haiku-20240307", - name: "Claude Haiku 3", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3-opus-20240229": { - id: "claude-3-opus-20240229", - name: "Claude Opus 3", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3-sonnet-20240229": { - id: "claude-3-sonnet-20240229", - name: "Claude Sonnet 3", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-fable-5": { - id: "claude-fable-5", - name: "Claude Fable 5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-0": { - id: "claude-opus-4-0", - name: "Claude Opus 4 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude Opus 4.1", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude Opus 4", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-8": { - id: "claude-opus-4-8", - name: "Claude Opus 4.8", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-0": { - id: "claude-sonnet-4-0", - name: "Claude Sonnet 4 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - }, - "azure-openai-responses": { - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"azure-openai-responses">, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"azure-openai-responses">, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"azure-openai-responses">, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"azure-openai-responses">, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"azure-openai-responses">, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-2024-05-13": { - id: "gpt-4o-2024-05-13", - name: "GPT-4o (2024-05-13)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-2024-08-06": { - id: "gpt-4o-2024-08-06", - name: "GPT-4o (2024-08-06)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-2024-11-20": { - id: "gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "GPT-5 Chat Latest", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "GPT-5.1 Chat", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "GPT-5.2 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat (latest)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.5-pro": { - id: "gpt-5.5-pro", - name: "GPT-5.5 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "o1": { - id: "o1", - name: "o1", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o1-pro": { - id: "o1-pro", - name: "o1-pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 150, - output: 600, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3": { - id: "o3", - name: "o3", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3-deep-research": { - id: "o3-deep-research", - name: "o3-deep-research", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o4-mini-deep-research": { - id: "o4-mini-deep-research", - name: "o4-mini-deep-research", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - }, - "cerebras": { - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.35, - output: 0.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 40960, - } satisfies Model<"openai-completions">, - "zai-glm-4.7": { - id: "zai-glm-4.7", - name: "Z.AI GLM-4.7", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 2.25, - output: 2.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 40960, - } satisfies Model<"openai-completions">, - }, - "cloudflare-ai-gateway": { - "claude-3-5-haiku": { - id: "claude-3-5-haiku", - name: "Claude Haiku 3.5 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-haiku": { - id: "claude-3-haiku", - name: "Claude Haiku 3", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3-opus": { - id: "claude-3-opus", - name: "Claude Opus 3", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3-sonnet": { - id: "claude-3-sonnet", - name: "Claude Sonnet 3", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3.5-haiku": { - id: "claude-3.5-haiku", - name: "Claude Haiku 3.5 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3.5-sonnet": { - id: "claude-3.5-sonnet", - name: "Claude Sonnet 3.5 v2", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-fable-5": { - id: "claude-fable-5", - name: "Claude Fable 5", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4": { - id: "claude-opus-4", - name: "Claude Opus 4 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-8": { - id: "claude-opus-4-8", - name: "Claude Opus 4.8", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-responses">, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-responses">, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "o1": { - id: "o1", - name: "o1", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3": { - id: "o3", - name: "o3", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.28, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "workers-ai/@cf/moonshotai/kimi-k2.5": { - id: "workers-ai/@cf/moonshotai/kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "workers-ai/@cf/moonshotai/kimi-k2.6": { - id: "workers-ai/@cf/moonshotai/kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "workers-ai/@cf/nvidia/nemotron-3-120b-a12b": { - id: "workers-ai/@cf/nvidia/nemotron-3-120b-a12b", - name: "Nemotron 3 Super 120B", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "workers-ai/@cf/zai-org/glm-4.7-flash": { - id: "workers-ai/@cf/zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "cloudflare-workers-ai": { - "@cf/google/gemma-4-26b-a4b-it": { - id: "@cf/google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "@cf/ibm-granite/granite-4.0-h-micro": { - id: "@cf/ibm-granite/granite-4.0-h-micro", - name: "Granite 4.0 H Micro", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: false, - input: ["text"], - cost: { - input: 0.017, - output: 0.112, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 131000, - } satisfies Model<"openai-completions">, - "@cf/meta/llama-3.3-70b-instruct-fp8-fast": { - id: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", - name: "Llama 3.3 70B Instruct fp8 Fast", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: false, - input: ["text"], - cost: { - input: 0.293, - output: 2.253, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 24000, - maxTokens: 24000, - } satisfies Model<"openai-completions">, - "@cf/meta/llama-4-scout-17b-16e-instruct": { - id: "@cf/meta/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.27, - output: 0.85, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "@cf/mistralai/mistral-small-3.1-24b-instruct": { - id: "@cf/mistralai/mistral-small-3.1-24b-instruct", - name: "Mistral Small 3.1 24B Instruct", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: false, - input: ["text"], - cost: { - input: 0.351, - output: 0.555, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "@cf/moonshotai/kimi-k2.6": { - id: "@cf/moonshotai/kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "@cf/moonshotai/kimi-k2.7-code": { - id: "@cf/moonshotai/kimi-k2.7-code", - name: "Kimi K2.7 Code", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "@cf/nvidia/nemotron-3-120b-a12b": { - id: "@cf/nvidia/nemotron-3-120b-a12b", - name: "Nemotron 3 Super 120B", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "@cf/openai/gpt-oss-120b": { - id: "@cf/openai/gpt-oss-120b", - name: "GPT OSS 120B", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.35, - output: 0.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "@cf/openai/gpt-oss-20b": { - id: "@cf/openai/gpt-oss-20b", - name: "GPT OSS 20B", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.2, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "@cf/qwen/qwen3-30b-a3b-fp8": { - id: "@cf/qwen/qwen3-30b-a3b-fp8", - name: "Qwen3 30B A3b fp8", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.0509, - output: 0.335, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "@cf/zai-org/glm-4.7-flash": { - id: "@cf/zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.0605, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "@cf/zai-org/glm-5.2": { - id: "@cf/zai-org/glm-5.2", - name: "Glm 5.2", - api: "openai-completions", - provider: "cloudflare-workers-ai", - baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - }, - "deepseek": { - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "openai-completions", - provider: "deepseek", - baseUrl: "https://api.deepseek.com", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - provider: "deepseek", - baseUrl: "https://api.deepseek.com", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.003625, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - }, - "fireworks": { - "accounts/fireworks/models/deepseek-v4-flash": { - id: "accounts/fireworks/models/deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/deepseek-v4-pro": { - id: "accounts/fireworks/models/deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 1.74, - output: 3.48, - cacheRead: 0.145, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/glm-5p1": { - id: "accounts/fireworks/models/glm-5p1", - name: "GLM 5.1", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/glm-5p2": { - id: "accounts/fireworks/models/glm-5p2", - name: "GLM 5.2", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/gpt-oss-120b": { - id: "accounts/fireworks/models/gpt-oss-120b", - name: "GPT OSS 120B", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/gpt-oss-20b": { - id: "accounts/fireworks/models/gpt-oss-20b", - name: "GPT OSS 20B", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.3, - cacheRead: 0.035, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/kimi-k2p6": { - id: "accounts/fireworks/models/kimi-k2p6", - name: "Kimi K2.6", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/kimi-k2p7-code": { - id: "accounts/fireworks/models/kimi-k2p7-code", - name: "Kimi K2.7 Code", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/minimax-m2p7": { - id: "accounts/fireworks/models/minimax-m2p7", - name: "MiniMax-M2.7", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 196608, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/minimax-m3": { - id: "accounts/fireworks/models/minimax-m3", - name: "MiniMax-M3", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 512000, - maxTokens: 512000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/qwen3p7-plus": { - id: "accounts/fireworks/models/qwen3p7-plus", - name: "Qwen 3.7 Plus", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/routers/glm-5p1-fast": { - id: "accounts/fireworks/routers/glm-5p1-fast", - name: "GLM 5.1 Fast", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 2.8, - output: 8.8, - cacheRead: 0.52, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/routers/kimi-k2p6-fast": { - id: "accounts/fireworks/routers/kimi-k2p6-fast", - name: "Kimi K2.6 Fast", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/routers/kimi-k2p6-turbo": { - id: "accounts/fireworks/routers/kimi-k2p6-turbo", - name: "Kimi K2.6 Turbo", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - "accounts/fireworks/routers/kimi-k2p7-code-fast": { - id: "accounts/fireworks/routers/kimi-k2p7-code-fast", - name: "Kimi K2.7 Code Fast", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.9, - output: 8, - cacheRead: 0.38, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - }, - "github-copilot": { - "claude-fable-5": { - id: "claude-fable-5", - name: "Claude Fable 5", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "claude-haiku-4.5": { - id: "claude-haiku-4.5", - name: "Claude Haiku 4.5 (latest)", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsEagerToolInputStreaming":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.5": { - id: "claude-opus-4.5", - name: "Claude Opus 4.5 (latest)", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.6": { - id: "claude-opus-4.6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.7": { - id: "claude-opus-4.7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.8": { - id: "claude-opus-4.8", - name: "Claude Opus 4.8", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4 (latest)", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsEagerToolInputStreaming":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 216000, - maxTokens: 16000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4.5": { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5 (latest)", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsEagerToolInputStreaming":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4.6": { - id: "claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"minimal":"low","xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gemini-3.5-flash": { - id: "gemini-3.5-flash", - name: "Gemini 3.5 Flash", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low"}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 264000, - maxTokens: 64000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "raptor-mini": { - id: "raptor-mini", - name: "Raptor mini", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - }, - "google": { - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash-Lite", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash-Lite", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-flash-lite": { - id: "gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-pro-preview-customtools": { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.5-flash": { - id: "gemini-3.5-flash", - name: "Gemini 3.5 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-flash-latest": { - id: "gemini-flash-latest", - name: "Gemini Flash Latest", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-flash-lite-latest": { - id: "gemini-flash-lite-latest", - name: "Gemini Flash-Lite Latest", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemma-4-26b-a4b-it": { - id: "gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"google-generative-ai">, - "gemma-4-31b-it": { - id: "gemma-4-31b-it", - name: "Gemma 4 31B IT", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"google-generative-ai">, - "gemma-4-E2B-it": { - id: "gemma-4-E2B-it", - name: "Gemma 4 E2B IT", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemma-4-E4B-it": { - id: "gemma-4-E4B-it", - name: "Gemma 4 E4B IT", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - }, - "google-vertex": { - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash-Lite", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-3.1-flash-lite": { - id: "gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-3.1-pro-preview-customtools": { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-3.5-flash": { - id: "gemini-3.5-flash", - name: "Gemini 3.5 Flash", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-flash-latest": { - id: "gemini-flash-latest", - name: "Gemini Flash Latest", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-flash-lite-latest": { - id: "gemini-flash-lite-latest", - name: "Gemini Flash-Lite Latest", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - }, - "groq": { - "llama-3.1-8b-instant": { - id: "llama-3.1-8b-instant", - name: "Llama 3.1 8B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.05, - output: 0.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "llama-3.3-70b-versatile": { - id: "llama-3.3-70b-versatile", - name: "Llama 3.3 70B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.59, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-scout-17b-16e-instruct": { - id: "meta-llama/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.11, - output: 0.34, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.0375, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "Safety GPT OSS 20B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.037, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3-32b": { - id: "qwen/qwen3-32b", - name: "Qwen3-32B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"default"}, - input: ["text"], - cost: { - input: 0.29, - output: 0.59, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 40960, - } satisfies Model<"openai-completions">, - }, - "huggingface": { - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMax-M2.1", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "MiniMaxAI/MiniMax-M2.7": { - id: "MiniMaxAI/MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3-235B-A22B-Thinking-2507", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen3-Coder-480B-A35B-Instruct", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 66536, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-Next": { - id: "Qwen/Qwen3-Coder-Next", - name: "Qwen3-Coder-Next", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen3-Next-80B-A3B-Instruct", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 66536, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen3-Next-80B-A3B-Thinking", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5-397B-A17B", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "XiaomiMiMo/MiMo-V2-Flash": { - id: "XiaomiMiMo/MiMo-V2-Flash", - name: "MiMo-V2-Flash", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek-R1-0528", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 3, - output: 5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek-V3.2", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.28, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V4-Pro": { - id: "deepseek-ai/DeepSeek-V4-Pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.003625, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 393216, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2-Instruct": { - id: "moonshotai/Kimi-K2-Instruct", - name: "Kimi-K2-Instruct", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi-K2-Instruct-0905", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi-K2-Thinking", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi-K2.5", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.6": { - id: "moonshotai/Kimi-K2.6", - name: "Kimi-K2.6", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "zai-org/GLM-4.7-Flash": { - id: "zai-org/GLM-4.7-Flash", - name: "GLM-4.7-Flash", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: {"supportsDeveloperRole":false}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "kimi-coding": { - "k2p7": { - id: "k2p7", - name: "Kimi K2.7 Code", - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: "https://api.kimi.com/coding", - headers: {"User-Agent":"KimiCLI/1.5"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "kimi-for-coding": { - id: "kimi-for-coding", - name: "Kimi For Coding", - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: "https://api.kimi.com/coding", - headers: {"User-Agent":"KimiCLI/1.5"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: "https://api.kimi.com/coding", - headers: {"User-Agent":"KimiCLI/1.5"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - }, - "minimax": { - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M3": { - id: "MiniMax-M3", - name: "MiniMax-M3", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.12, - cacheWrite: 0, - }, - contextWindow: 512000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, - "minimax-cn": { - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M3": { - id: "MiniMax-M3", - name: "MiniMax-M3", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.12, - cacheWrite: 0, - }, - contextWindow: 512000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, - "mistral": { - "codestral-latest": { - id: "codestral-latest", - name: "Codestral (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.9, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"mistral-conversations">, - "devstral-2512": { - id: "devstral-2512", - name: "Devstral 2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "devstral-latest": { - id: "devstral-latest", - name: "Devstral 2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "devstral-medium-2507": { - id: "devstral-medium-2507", - name: "Devstral Medium", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "devstral-medium-latest": { - id: "devstral-medium-latest", - name: "Devstral 2 (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "devstral-small-2505": { - id: "devstral-small-2505", - name: "Devstral Small 2505", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "devstral-small-2507": { - id: "devstral-small-2507", - name: "Devstral Small", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "labs-devstral-small-2512": { - id: "labs-devstral-small-2512", - name: "Devstral Small 2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"mistral-conversations">, - "magistral-medium-latest": { - id: "magistral-medium-latest", - name: "Magistral Medium (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text"], - cost: { - input: 2, - output: 5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"mistral-conversations">, - "magistral-small": { - id: "magistral-small", - name: "Magistral Small", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "ministral-3b-latest": { - id: "ministral-3b-latest", - name: "Ministral 3B (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "ministral-8b-latest": { - id: "ministral-8b-latest", - name: "Ministral 8B (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral Large 2.1", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"mistral-conversations">, - "mistral-large-2512": { - id: "mistral-large-2512", - name: "Mistral Large 3", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-large-latest": { - id: "mistral-large-latest", - name: "Mistral Large (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-medium-2505": { - id: "mistral-medium-2505", - name: "Mistral Medium 3", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"mistral-conversations">, - "mistral-medium-2508": { - id: "mistral-medium-2508", - name: "Mistral Medium 3.1", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-medium-2604": { - id: "mistral-medium-2604", - name: "Mistral Medium 3.5", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-medium-3.5": { - id: "mistral-medium-3.5", - name: "Mistral Medium 3.5", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-medium-latest": { - id: "mistral-medium-latest", - name: "Mistral Medium (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "mistral-small-2506": { - id: "mistral-small-2506", - name: "Mistral Small 3.2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"mistral-conversations">, - "mistral-small-2603": { - id: "mistral-small-2603", - name: "Mistral Small 4", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"mistral-conversations">, - "mistral-small-latest": { - id: "mistral-small-latest", - name: "Mistral Small (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"mistral-conversations">, - "open-mistral-7b": { - id: "open-mistral-7b", - name: "Mistral 7B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 0.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8000, - maxTokens: 8000, - } satisfies Model<"mistral-conversations">, - "open-mistral-nemo": { - id: "open-mistral-nemo", - name: "Open Mistral Nemo", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "open-mixtral-8x22b": { - id: "open-mixtral-8x22b", - name: "Mixtral 8x22B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 64000, - maxTokens: 64000, - } satisfies Model<"mistral-conversations">, - "open-mixtral-8x7b": { - id: "open-mixtral-8x7b", - name: "Mixtral 8x7B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.7, - output: 0.7, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 32000, - } satisfies Model<"mistral-conversations">, - "pixtral-12b": { - id: "pixtral-12b", - name: "Pixtral 12B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "pixtral-large-latest": { - id: "pixtral-large-latest", - name: "Pixtral Large (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - }, - "moonshotai": { - "kimi-k2-0711-preview": { - id: "kimi-k2-0711-preview", - name: "Kimi K2 0711", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "kimi-k2-0905-preview": { - id: "kimi-k2-0905-preview", - name: "Kimi K2 0905", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-turbo-preview": { - id: "kimi-k2-turbo-preview", - name: "Kimi K2 Turbo", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 2.4, - output: 10, - cacheRead: 0.6, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.7-code": { - id: "kimi-k2.7-code", - name: "Kimi K2.7 Code", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.7-code-highspeed": { - id: "kimi-k2.7-code-highspeed", - name: "Kimi K2.7 Code HighSpeed", - api: "openai-completions", - provider: "moonshotai", - baseUrl: "https://api.moonshot.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.9, - output: 8, - cacheRead: 0.38, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - }, - "moonshotai-cn": { - "kimi-k2-0711-preview": { - id: "kimi-k2-0711-preview", - name: "Kimi K2 0711", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "kimi-k2-0905-preview": { - id: "kimi-k2-0905-preview", - name: "Kimi K2 0905", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2-turbo-preview": { - id: "kimi-k2-turbo-preview", - name: "Kimi K2 Turbo", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: false, - input: ["text"], - cost: { - input: 2.4, - output: 10, - cacheRead: 0.6, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.7-code": { - id: "kimi-k2.7-code", - name: "Kimi K2.7 Code", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "kimi-k2.7-code-highspeed": { - id: "kimi-k2.7-code-highspeed", - name: "Kimi K2.7 Code HighSpeed", - api: "openai-completions", - provider: "moonshotai-cn", - baseUrl: "https://api.moonshot.cn/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.9, - output: 8, - cacheRead: 0.38, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - }, - "nvidia": { - "meta/llama-3.1-70b-instruct": { - id: "meta/llama-3.1-70b-instruct", - name: "Llama 3.1 70b Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta/llama-3.1-8b-instruct": { - id: "meta/llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta/llama-3.2-11b-vision-instruct": { - id: "meta/llama-3.2-11b-vision-instruct", - name: "Llama 3.2 11b Vision Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta/llama-3.2-90b-vision-instruct": { - id: "meta/llama-3.2-90b-vision-instruct", - name: "Llama-3.2-90B-Vision-Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "meta/llama-3.3-70b-instruct": { - id: "meta/llama-3.3-70b-instruct", - name: "Llama 3.3 70b Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large-3-675b-instruct-2512": { - id: "mistralai/mistral-large-3-675b-instruct-2512", - name: "Mistral Large 3 675B Instruct 2512", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "mistralai/mistral-small-4-119b-2603": { - id: "mistralai/mistral-small-4-119b-2603", - name: "mistral-small-4-119b-2603", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "nemotron-3-nano-30b-a3b", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": { - id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", - name: "Nemotron 3 Nano Omni", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "Nemotron 3 Super", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.2, - output: 0.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-ultra-550b-a55b": { - id: "nvidia/nemotron-3-ultra-550b-a55b", - name: "Nemotron 3 Ultra 550B A55B", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "nvidia/nvidia-nemotron-nano-9b-v2": { - id: "nvidia/nvidia-nemotron-nano-9b-v2", - name: "nvidia-nemotron-nano-9b-v2", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS-120B", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen3.5 122B-A10B", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "stepfun-ai/step-3.5-flash": { - id: "stepfun-ai/step-3.5-flash", - name: "Step 3.5 Flash", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "stepfun-ai/step-3.7-flash": { - id: "stepfun-ai/step-3.7-flash", - name: "Step 3.7 Flash", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "openai": { - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-responses">, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-responses">, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-responses">, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-responses">, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-responses">, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-2024-05-13": { - id: "gpt-4o-2024-05-13", - name: "GPT-4o (2024-05-13)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-responses">, - "gpt-4o-2024-08-06": { - id: "gpt-4o-2024-08-06", - name: "GPT-4o (2024-08-06)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-2024-11-20": { - id: "gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "GPT-5 Chat Latest", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none"}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "GPT-5.1 Chat", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "GPT-5.2 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat (latest)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":"none","xhigh":"xhigh","minimal":null}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5-pro": { - id: "gpt-5.5-pro", - name: "GPT-5.5 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "o1": { - id: "o1", - name: "o1", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o1-pro": { - id: "o1-pro", - name: "o1-pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 150, - output: 600, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3": { - id: "o3", - name: "o3", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-deep-research": { - id: "o3-deep-research", - name: "o3-deep-research", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o4-mini-deep-research": { - id: "o4-mini-deep-research", - name: "o4-mini-deep-research", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - }, - "openai-codex": { - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - }, - "opencode": { - "big-pickle": { - id: "big-pickle", - name: "Big Pickle", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-8": { - id: "claude-opus-4-8", - name: "Claude Opus 4.8", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "deepseek-v4-flash-free": { - id: "deepseek-v4-flash-free", - name: "DeepSeek V4 Flash Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 1.74, - output: 3.84, - cacheRead: 0.145, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "gemini-3-flash": { - id: "gemini-3-flash", - name: "Gemini 3 Flash", - api: "google-generative-ai", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-pro": { - id: "gemini-3.1-pro", - name: "Gemini 3.1 Pro Preview", - api: "google-generative-ai", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.5-flash": { - id: "gemini-3.5-flash", - name: "Gemini 3.5 Flash", - api: "google-generative-ai", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "glm-5": { - id: "glm-5", - name: "GLM-5", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 30, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5-pro": { - id: "gpt-5.5-pro", - name: "GPT-5.5 Pro", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 30, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "grok-build-0.1": { - id: "grok-build-0.1", - name: "Grok Build 0.1", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 1, - output: 2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "mimo-v2.5-free": { - id: "mimo-v2.5-free", - name: "MiMo V2.5 Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax M2.7", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "nemotron-3-ultra-free": { - id: "nemotron-3-ultra-free", - name: "Nemotron 3 Ultra Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "north-mini-code-free": { - id: "north-mini-code-free", - name: "North Mini Code Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.2, - cacheRead: 0.02, - cacheWrite: 0.25, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0.625, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - }, - "opencode-go": { - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 1.74, - output: 3.48, - cacheRead: 0.0145, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "glm-5.2": { - id: "glm-5.2", - name: "GLM-5.2", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "kimi-k2.7-code": { - id: "kimi-k2.7-code", - name: "Kimi K2.7 Code", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo V2.5", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo V2.5 Pro", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.74, - output: 3.48, - cacheRead: 0.0145, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax M2.7", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "minimax-m3": { - id: "minimax-m3", - name: "MiniMax M3 (3x usage)", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 512000, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"thinkingFormat":"qwen","maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0.625, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen3.7-max": { - id: "qwen3.7-max", - name: "Qwen3.7 Max", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text"], - cost: { - input: 2.5, - output: 7.5, - cacheRead: 0.5, - cacheWrite: 3.125, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "qwen3.7-plus": { - id: "qwen3.7-plus", - name: "Qwen3.7 Plus", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.04, - cacheWrite: 0.5, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - }, - "openrouter": { - "ai21/jamba-large-1.7": { - id: "ai21/jamba-large-1.7", - name: "AI21: Jamba Large 1.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "amazon/nova-2-lite-v1": { - id: "amazon/nova-2-lite-v1", - name: "Amazon: Nova 2 Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "amazon/nova-lite-v1": { - id: "amazon/nova-lite-v1", - name: "Amazon: Nova Lite 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.06, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 5120, - } satisfies Model<"openai-completions">, - "amazon/nova-micro-v1": { - id: "amazon/nova-micro-v1", - name: "Amazon: Nova Micro 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.035, - output: 0.14, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 5120, - } satisfies Model<"openai-completions">, - "amazon/nova-premier-v1": { - id: "amazon/nova-premier-v1", - name: "Amazon: Nova Premier 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 12.5, - cacheRead: 0.625, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "amazon/nova-pro-v1": { - id: "amazon/nova-pro-v1", - name: "Amazon: Nova Pro 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 3.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 5120, - } satisfies Model<"openai-completions">, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Anthropic: Claude 3 Haiku", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "anthropic/claude-fable-5": { - id: "anthropic/claude-fable-5", - name: "Anthropic: Claude Fable 5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Anthropic: Claude Haiku 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Anthropic: Claude Opus 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Anthropic: Claude Opus 4.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Anthropic: Claude Opus 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Anthropic: Claude Opus 4.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.6-fast": { - id: "anthropic/claude-opus-4.6-fast", - name: "Anthropic: Claude Opus 4.6 (Fast)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 30, - output: 150, - cacheRead: 3, - cacheWrite: 37.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Anthropic: Claude Opus 4.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.7-fast": { - id: "anthropic/claude-opus-4.7-fast", - name: "Anthropic: Claude Opus 4.7 (Fast)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 150, - cacheRead: 3, - cacheWrite: 37.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.8": { - id: "anthropic/claude-opus-4.8", - name: "Anthropic: Claude Opus 4.8", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.8-fast": { - id: "anthropic/claude-opus-4.8-fast", - name: "Anthropic: Claude Opus 4.8 (Fast)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Anthropic: Claude Sonnet 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Anthropic: Claude Sonnet 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Anthropic: Claude Sonnet 4.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Arcee AI: Trinity Large Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.22, - output: 0.85, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "arcee-ai/trinity-mini": { - id: "arcee-ai/trinity-mini", - name: "Arcee AI: Trinity Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.045, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "arcee-ai/virtuoso-large": { - id: "arcee-ai/virtuoso-large", - name: "Arcee AI: Virtuoso Large", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.75, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "auto": { - id: "auto", - name: "Auto", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-1.6": { - id: "bytedance-seed/seed-1.6", - name: "ByteDance Seed: Seed 1.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-1.6-flash": { - id: "bytedance-seed/seed-1.6-flash", - name: "ByteDance Seed: Seed 1.6 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-2.0-lite": { - id: "bytedance-seed/seed-2.0-lite", - name: "ByteDance Seed: Seed-2.0-Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-2.0-mini": { - id: "bytedance-seed/seed-2.0-mini", - name: "ByteDance Seed: Seed-2.0-Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "cohere/command-r-08-2024": { - id: "cohere/command-r-08-2024", - name: "Cohere: Command R (08-2024)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"openai-completions">, - "cohere/command-r-plus-08-2024": { - id: "cohere/command-r-plus-08-2024", - name: "Cohere: Command R+ (08-2024)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"openai-completions">, - "cohere/north-mini-code:free": { - id: "cohere/north-mini-code:free", - name: "Cohere: North Mini Code (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-chat": { - id: "deepseek/deepseek-chat", - name: "DeepSeek: DeepSeek V3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.2002, - output: 0.8001, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16000, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-chat-v3-0324": { - id: "deepseek/deepseek-chat-v3-0324", - name: "DeepSeek: DeepSeek V3 0324", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.77, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-chat-v3.1": { - id: "deepseek/deepseek-chat-v3.1", - name: "DeepSeek: DeepSeek V3.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.21, - output: 0.79, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek: R1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.7, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 16000, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek: R1 0528", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 2.15, - cacheRead: 0.35, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek: DeepSeek V3.1 Terminus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.27, - output: 0.95, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek: DeepSeek V3.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.2288, - output: 0.3432, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek: DeepSeek V3.2 Exp", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.27, - output: 0.41, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v4-flash": { - id: "deepseek/deepseek-v4-flash", - name: "DeepSeek: DeepSeek V4 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, - input: ["text"], - cost: { - input: 0.09, - output: 0.18, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v4-pro": { - id: "deepseek/deepseek-v4-pro", - name: "DeepSeek: DeepSeek V4 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, - input: ["text"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.003625, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "essentialai/rnj-1-instruct": { - id: "essentialai/rnj-1-instruct", - name: "EssentialAI: Rnj 1 Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Google: Gemini 2.5 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0.083333, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Google: Gemini 2.5 Flash Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0.083333, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-flash-lite-preview-09-2025": { - id: "google/gemini-2.5-flash-lite-preview-09-2025", - name: "Google: Gemini 2.5 Flash Lite Preview 09-2025", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0.083333, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Google: Gemini 2.5 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-pro-preview": { - id: "google/gemini-2.5-pro-preview", - name: "Google: Gemini 2.5 Pro Preview 06-05", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-pro-preview-05-06": { - id: "google/gemini-2.5-pro-preview-05-06", - name: "Google: Gemini 2.5 Pro Preview 05-06", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Google: Gemini 3 Flash Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0.083333, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-3-pro-image": { - id: "google/gemini-3-pro-image", - name: "Google: Nano Banana Pro (Gemini 3 Pro Image)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0.375, - }, - contextWindow: 65536, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-flash-lite": { - id: "google/gemini-3.1-flash-lite", - name: "Google: Gemini 3.1 Flash Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.025, - cacheWrite: 0.083333, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Google: Gemini 3.1 Flash Lite Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.025, - cacheWrite: 0.083333, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Google: Gemini 3.1 Pro Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-pro-preview-customtools": { - id: "google/gemini-3.1-pro-preview-customtools", - name: "Google: Gemini 3.1 Pro Preview Custom Tools", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0.375, - }, - contextWindow: 1048756, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.5-flash": { - id: "google/gemini-3.5-flash", - name: "Google: Gemini 3.5 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0.083333, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemma-3-12b-it": { - id: "google/gemma-3-12b-it", - name: "Google: Gemma 3 12B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Google: Gemma 3 27B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.08, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Google: Gemma 4 26B A4B ", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.06, - output: 0.33, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "google/gemma-4-26b-a4b-it:free": { - id: "google/gemma-4-26b-a4b-it:free", - name: "Google: Gemma 4 26B A4B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Google: Gemma 4 31B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.12, - output: 0.35, - cacheRead: 0.09, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "google/gemma-4-31b-it:free": { - id: "google/gemma-4-31b-it:free", - name: "Google: Gemma 4 31B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "ibm-granite/granite-4.1-8b": { - id: "ibm-granite/granite-4.1-8b", - name: "IBM: Granite 4.1 8B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.05, - output: 0.1, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Inception: Mercury 2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text"], - cost: { - input: 0.25, - output: 0.75, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 50000, - } satisfies Model<"openai-completions">, - "inclusionai/ling-2.6-1t": { - id: "inclusionai/ling-2.6-1t", - name: "inclusionAI: Ling-2.6-1T", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.075, - output: 0.625, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "inclusionai/ling-2.6-flash": { - id: "inclusionai/ling-2.6-flash", - name: "inclusionAI: Ling-2.6-flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.01, - output: 0.03, - cacheRead: 0.002, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "inclusionai/ring-2.6-1t": { - id: "inclusionai/ring-2.6-1t", - name: "inclusionAI: Ring-2.6-1T", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.625, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "kwaipilot/kat-coder-pro-v2": { - id: "kwaipilot/kat-coder-pro-v2", - name: "Kwaipilot: KAT-Coder-Pro V2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 80000, - } satisfies Model<"openai-completions">, - "liquid/lfm-2.5-1.2b-thinking:free": { - id: "liquid/lfm-2.5-1.2b-thinking:free", - name: "LiquidAI: LFM2.5-1.2B-Thinking (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.1-70b-instruct": { - id: "meta-llama/llama-3.1-70b-instruct", - name: "Meta: Llama 3.1 70B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.1-8b-instruct": { - id: "meta-llama/llama-3.1-8b-instruct", - name: "Meta: Llama 3.1 8B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.02, - output: 0.03, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.3-70b-instruct": { - id: "meta-llama/llama-3.3-70b-instruct", - name: "Meta: Llama 3.3 70B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.32, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.3-70b-instruct:free": { - id: "meta-llama/llama-3.3-70b-instruct:free", - name: "Meta: Llama 3.3 70B Instruct (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-maverick": { - id: "meta-llama/llama-4-maverick", - name: "Meta: Llama 4 Maverick", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-scout": { - id: "meta-llama/llama-4-scout", - name: "Meta: Llama 4 Scout", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 10000000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "minimax/minimax-m1": { - id: "minimax/minimax-m1", - name: "MiniMax: MiniMax M1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.4, - output: 2.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 40000, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax: MiniMax M2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.255, - output: 1, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 196608, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax: MiniMax M2.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.29, - output: 0.95, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 196608, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax: MiniMax M2.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.9, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 196608, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax: MiniMax M2.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 1, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "minimax/minimax-m3": { - id: "minimax/minimax-m3", - name: "MiniMax: MiniMax M3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 512000, - } satisfies Model<"openai-completions">, - "mistralai/codestral-2508": { - id: "mistralai/codestral-2508", - name: "Mistral: Codestral 2508", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.9, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/devstral-2512": { - id: "mistralai/devstral-2512", - name: "Mistral: Devstral 2 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/ministral-14b-2512": { - id: "mistralai/ministral-14b-2512", - name: "Mistral: Ministral 3 14B 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.2, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/ministral-3b-2512": { - id: "mistralai/ministral-3b-2512", - name: "Mistral: Ministral 3 3B 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/ministral-8b-2512": { - id: "mistralai/ministral-8b-2512", - name: "Mistral: Ministral 3 8B 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large": { - id: "mistralai/mistral-large", - name: "Mistral Large", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large-2407": { - id: "mistralai/mistral-large-2407", - name: "Mistral Large 2407", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large-2512": { - id: "mistralai/mistral-large-2512", - name: "Mistral: Mistral Large 3 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-medium-3": { - id: "mistralai/mistral-medium-3", - name: "Mistral: Mistral Medium 3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-medium-3-5": { - id: "mistralai/mistral-medium-3-5", - name: "Mistral: Mistral Medium 3.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-medium-3.1": { - id: "mistralai/mistral-medium-3.1", - name: "Mistral: Mistral Medium 3.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-nemo": { - id: "mistralai/mistral-nemo", - name: "Mistral: Mistral Nemo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.02, - output: 0.03, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-saba": { - id: "mistralai/mistral-saba", - name: "Mistral: Saba", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-small-2603": { - id: "mistralai/mistral-small-2603", - name: "Mistral: Mistral Small 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-small-3.2-24b-instruct": { - id: "mistralai/mistral-small-3.2-24b-instruct", - name: "Mistral: Mistral Small 3.2 24B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "mistralai/mixtral-8x22b-instruct": { - id: "mistralai/mixtral-8x22b-instruct", - name: "Mistral: Mixtral 8x22B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 65536, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/voxtral-small-24b-2507": { - id: "mistralai/voxtral-small-24b-2507", - name: "Mistral: Voxtral Small 24B 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "MoonshotAI: Kimi K2 0711", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.57, - output: 2.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "MoonshotAI: Kimi K2 0905", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "MoonshotAI: Kimi K2 Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "MoonshotAI: Kimi K2.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.41, - output: 2.06, - cacheRead: 0.07, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "MoonshotAI: Kimi K2.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.68, - output: 3.41, - cacheRead: 0.34, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262142, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.7-code": { - id: "moonshotai/kimi-k2.7-code", - name: "MoonshotAI: Kimi K2.7 Code", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.74, - output: 3.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "nex-agi/nex-n2-pro:free": { - id: "nex-agi/nex-n2-pro:free", - name: "Nex AGI: Nex-N2-Pro (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - name: "NVIDIA: Llama 3.3 Nemotron Super 49B V1.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.4, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "NVIDIA: Nemotron 3 Nano 30B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.05, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 228000, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-30b-a3b:free": { - id: "nvidia/nemotron-3-nano-30b-a3b:free", - name: "NVIDIA: Nemotron 3 Nano 30B A3B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free": { - id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", - name: "NVIDIA: Nemotron 3 Nano Omni (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "NVIDIA: Nemotron 3 Super", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09, - output: 0.45, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-super-120b-a12b:free": { - id: "nvidia/nemotron-3-super-120b-a12b:free", - name: "NVIDIA: Nemotron 3 Super (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-ultra-550b-a55b": { - id: "nvidia/nemotron-3-ultra-550b-a55b", - name: "NVIDIA: Nemotron 3 Ultra", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 2.2, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-ultra-550b-a55b:free": { - id: "nvidia/nemotron-3-ultra-550b-a55b:free", - name: "NVIDIA: Nemotron 3 Ultra (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-nano-12b-v2-vl:free": { - id: "nvidia/nemotron-nano-12b-v2-vl:free", - name: "NVIDIA: Nemotron Nano 12B 2 VL (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-nano-9b-v2:free": { - id: "nvidia/nemotron-nano-9b-v2:free", - name: "NVIDIA: Nemotron Nano 9B V2 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "OpenAI: GPT-3.5 Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16385, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-3.5-turbo-0613": { - id: "openai/gpt-3.5-turbo-0613", - name: "OpenAI: GPT-3.5 Turbo (older v0613)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 4095, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-3.5-turbo-16k": { - id: "openai/gpt-3.5-turbo-16k", - name: "OpenAI: GPT-3.5 Turbo 16k", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16385, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4": { - id: "openai/gpt-4", - name: "OpenAI: GPT-4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8191, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "OpenAI: GPT-4 Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4-turbo-preview": { - id: "openai/gpt-4-turbo-preview", - name: "OpenAI: GPT-4 Turbo Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "OpenAI: GPT-4.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "OpenAI: GPT-4.1 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "OpenAI: GPT-4.1 Nano", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "OpenAI: GPT-4o", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-2024-05-13": { - id: "openai/gpt-4o-2024-05-13", - name: "OpenAI: GPT-4o (2024-05-13)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-2024-08-06": { - id: "openai/gpt-4o-2024-08-06", - name: "OpenAI: GPT-4o (2024-08-06)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-2024-11-20": { - id: "openai/gpt-4o-2024-11-20", - name: "OpenAI: GPT-4o (2024-11-20)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "OpenAI: GPT-4o-mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-mini-2024-07-18": { - id: "openai/gpt-4o-mini-2024-07-18", - name: "OpenAI: GPT-4o-mini (2024-07-18)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "OpenAI: GPT-5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "OpenAI: GPT-5 Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "OpenAI: GPT-5 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "OpenAI: GPT-5 Nano", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "OpenAI: GPT-5 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "OpenAI: GPT-5.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "OpenAI: GPT-5.1 Chat", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "OpenAI: GPT-5.1-Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "OpenAI: GPT-5.1-Codex-Max", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "OpenAI: GPT-5.1-Codex-Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "OpenAI: GPT-5.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "OpenAI: GPT-5.2 Chat", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "OpenAI: GPT-5.2-Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "OpenAI: GPT-5.2 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "OpenAI: GPT-5.3 Chat", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "OpenAI: GPT-5.3-Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "OpenAI: GPT-5.4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "OpenAI: GPT-5.4 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "OpenAI: GPT-5.4 Nano", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "OpenAI: GPT-5.4 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "OpenAI: GPT-5.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.5-pro": { - id: "openai/gpt-5.5-pro", - name: "OpenAI: GPT-5.5 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","off":null,"minimal":null,"low":null}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-audio": { - id: "openai/gpt-audio", - name: "OpenAI: GPT Audio", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-audio-mini": { - id: "openai/gpt-audio-mini", - name: "OpenAI: GPT Audio Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-chat-latest": { - id: "openai/gpt-chat-latest", - name: "OpenAI: GPT Chat Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "OpenAI: gpt-oss-120b", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.039, - output: 0.18, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b:free": { - id: "openai/gpt-oss-120b:free", - name: "OpenAI: gpt-oss-120b (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "OpenAI: gpt-oss-20b", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.029, - output: 0.14, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b:free": { - id: "openai/gpt-oss-20b:free", - name: "OpenAI: gpt-oss-20b (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "OpenAI: gpt-oss-safeguard-20b", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.037, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "openai/o1": { - id: "openai/o1", - name: "OpenAI: o1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3": { - id: "openai/o3", - name: "OpenAI: o3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "OpenAI: o3 Deep Research", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "OpenAI: o3 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-mini-high": { - id: "openai/o3-mini-high", - name: "OpenAI: o3 Mini High", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "OpenAI: o3 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "OpenAI: o4 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o4-mini-deep-research": { - id: "openai/o4-mini-deep-research", - name: "OpenAI: o4 Mini Deep Research", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o4-mini-high": { - id: "openai/o4-mini-high", - name: "OpenAI: o4 Mini High", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openrouter/auto": { - id: "openrouter/auto", - name: "Auto Router", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: -1000000, - output: -1000000, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openrouter/free": { - id: "openrouter/free", - name: "Free Models Router", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openrouter/owl-alpha": { - id: "openrouter/owl-alpha", - name: "Owl Alpha", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048756, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "poolside/laguna-m.1:free": { - id: "poolside/laguna-m.1:free", - name: "Poolside: Laguna M.1 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "poolside/laguna-xs.2:free": { - id: "poolside/laguna-xs.2:free", - name: "Poolside: Laguna XS.2 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "Prime Intellect: INTELLECT-3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.2, - output: 1.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "qwen/qwen-2.5-72b-instruct": { - id: "qwen/qwen-2.5-72b-instruct", - name: "Qwen2.5 72B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.36, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen-2.5-7b-instruct": { - id: "qwen/qwen-2.5-7b-instruct", - name: "Qwen: Qwen2.5 7B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen-plus": { - id: "qwen/qwen-plus", - name: "Qwen: Qwen-Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.26, - output: 0.78, - cacheRead: 0.052, - cacheWrite: 0.325, - }, - contextWindow: 1000000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen-plus-2025-07-28": { - id: "qwen/qwen-plus-2025-07-28", - name: "Qwen: Qwen Plus 0728", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.26, - output: 0.78, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen-plus-2025-07-28:thinking": { - id: "qwen/qwen-plus-2025-07-28:thinking", - name: "Qwen: Qwen Plus 0728 (thinking)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.26, - output: 0.78, - cacheRead: 0, - cacheWrite: 0.325, - }, - contextWindow: 1000000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-14b": { - id: "qwen/qwen3-14b", - name: "Qwen: Qwen3 14B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131702, - maxTokens: 40960, - } satisfies Model<"openai-completions">, - "qwen/qwen3-235b-a22b": { - id: "qwen/qwen3-235b-a22b", - name: "Qwen: Qwen3 235B A22B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.455, - output: 1.82, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "qwen/qwen3-235b-a22b-2507": { - id: "qwen/qwen3-235b-a22b-2507", - name: "Qwen: Qwen3 235B A22B Instruct 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen: Qwen3 235B A22B Thinking 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3-30b-a3b": { - id: "qwen/qwen3-30b-a3b", - name: "Qwen: Qwen3 30B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.12, - output: 0.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-30b-a3b-instruct-2507": { - id: "qwen/qwen3-30b-a3b-instruct-2507", - name: "Qwen: Qwen3 30B A3B Instruct 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.04815, - output: 0.19305, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "qwen/qwen3-30b-a3b-thinking-2507": { - id: "qwen/qwen3-30b-a3b-thinking-2507", - name: "Qwen: Qwen3 30B A3B Thinking 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.08, - output: 0.4, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "qwen/qwen3-32b": { - id: "qwen/qwen3-32b", - name: "Qwen: Qwen3 32B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.08, - output: 0.28, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-8b": { - id: "qwen/qwen3-8b", - name: "Qwen: Qwen3 8B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder": { - id: "qwen/qwen3-coder", - name: "Qwen: Qwen3 Coder 480B A35B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 1.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-30b-a3b-instruct": { - id: "qwen/qwen3-coder-30b-a3b-instruct", - name: "Qwen: Qwen3 Coder 30B A3B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.07, - output: 0.27, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 160000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-flash": { - id: "qwen/qwen3-coder-flash", - name: "Qwen: Qwen3 Coder Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.195, - output: 0.975, - cacheRead: 0.039, - cacheWrite: 0.24375, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-next": { - id: "qwen/qwen3-coder-next", - name: "Qwen: Qwen3 Coder Next", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.11, - output: 0.8, - cacheRead: 0.07, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-plus": { - id: "qwen/qwen3-coder-plus", - name: "Qwen: Qwen3 Coder Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.65, - output: 3.25, - cacheRead: 0.13, - cacheWrite: 0.8125, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder:free": { - id: "qwen/qwen3-coder:free", - name: "Qwen: Qwen3 Coder 480B A35B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 262000, - } satisfies Model<"openai-completions">, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen: Qwen3 Max", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.78, - output: 3.9, - cacheRead: 0.156, - cacheWrite: 0.975, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-max-thinking": { - id: "qwen/qwen3-max-thinking", - name: "Qwen: Qwen3 Max Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.78, - output: 3.9, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen: Qwen3 Next 80B A3B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09, - output: 1.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-next-80b-a3b-instruct:free": { - id: "qwen/qwen3-next-80b-a3b-instruct:free", - name: "Qwen: Qwen3 Next 80B A3B Instruct (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen: Qwen3 Next 80B A3B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.0975, - output: 0.78, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-235b-a22b-instruct": { - id: "qwen/qwen3-vl-235b-a22b-instruct", - name: "Qwen: Qwen3 VL 235B A22B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.88, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-235b-a22b-thinking": { - id: "qwen/qwen3-vl-235b-a22b-thinking", - name: "Qwen: Qwen3 VL 235B A22B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.26, - output: 2.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-30b-a3b-instruct": { - id: "qwen/qwen3-vl-30b-a3b-instruct", - name: "Qwen: Qwen3 VL 30B A3B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.13, - output: 0.52, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-30b-a3b-thinking": { - id: "qwen/qwen3-vl-30b-a3b-thinking", - name: "Qwen: Qwen3 VL 30B A3B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.13, - output: 1.56, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-32b-instruct": { - id: "qwen/qwen3-vl-32b-instruct", - name: "Qwen: Qwen3 VL 32B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.104, - output: 0.416, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-8b-instruct": { - id: "qwen/qwen3-vl-8b-instruct", - name: "Qwen: Qwen3 VL 8B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.08, - output: 0.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-8b-thinking": { - id: "qwen/qwen3-vl-8b-thinking", - name: "Qwen: Qwen3 VL 8B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.117, - output: 1.365, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen: Qwen3.5-122B-A10B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.26, - output: 2.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-27b": { - id: "qwen/qwen3.5-27b", - name: "Qwen: Qwen3.5-27B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.195, - output: 1.56, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-35b-a3b": { - id: "qwen/qwen3.5-35b-a3b", - name: "Qwen: Qwen3.5-35B-A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 1, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 81920, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen: Qwen3.5 397B A17B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.385, - output: 2.45, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-9b": { - id: "qwen/qwen3.5-9b", - name: "Qwen: Qwen3.5-9B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-flash-02-23": { - id: "qwen/qwen3.5-flash-02-23", - name: "Qwen: Qwen3.5-Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.065, - output: 0.26, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-plus-02-15": { - id: "qwen/qwen3.5-plus-02-15", - name: "Qwen: Qwen3.5 Plus 2026-02-15", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.26, - output: 1.56, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-plus-20260420": { - id: "qwen/qwen3.5-plus-20260420", - name: "Qwen: Qwen3.5 Plus 2026-04-20", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.8, - cacheRead: 0, - cacheWrite: 0.375, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-27b": { - id: "qwen/qwen3.6-27b", - name: "Qwen: Qwen3.6 27B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2885, - output: 3.17, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262140, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-35b-a3b": { - id: "qwen/qwen3.6-35b-a3b", - name: "Qwen: Qwen3.6 35B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 1, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-flash": { - id: "qwen/qwen3.6-flash", - name: "Qwen: Qwen3.6 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1875, - output: 1.125, - cacheRead: 0, - cacheWrite: 0.234375, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-max-preview": { - id: "qwen/qwen3.6-max-preview", - name: "Qwen: Qwen3.6 Max Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.04, - output: 6.24, - cacheRead: 0, - cacheWrite: 1.3, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-plus": { - id: "qwen/qwen3.6-plus", - name: "Qwen: Qwen3.6 Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.325, - output: 1.95, - cacheRead: 0, - cacheWrite: 0.40625, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.7-max": { - id: "qwen/qwen3.7-max", - name: "Qwen: Qwen3.7 Max", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.25, - output: 3.75, - cacheRead: 0.25, - cacheWrite: 1.5625, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.7-plus": { - id: "qwen/qwen3.7-plus", - name: "Qwen: Qwen3.7 Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.32, - output: 1.28, - cacheRead: 0.064, - cacheWrite: 0.4, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "rekaai/reka-edge": { - id: "rekaai/reka-edge", - name: "Reka Edge", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16384, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "relace/relace-search": { - id: "relace/relace-search", - name: "Relace: Relace Search", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "sao10k/l3.1-euryale-70b": { - id: "sao10k/l3.1-euryale-70b", - name: "Sao10K: Llama 3.1 Euryale 70B v2.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.85, - output: 0.85, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "StepFun: Step 3.5 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09, - output: 0.3, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "stepfun/step-3.7-flash": { - id: "stepfun/step-3.7-flash", - name: "StepFun: Step 3.7 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.15, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "tencent/hy3-preview": { - id: "tencent/hy3-preview", - name: "Tencent: Hy3 preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.066, - output: 0.26, - cacheRead: 0.029, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "thedrummer/rocinante-12b": { - id: "thedrummer/rocinante-12b", - name: "TheDrummer: Rocinante 12B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.17, - output: 0.43, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "thedrummer/unslopnemo-12b": { - id: "thedrummer/unslopnemo-12b", - name: "TheDrummer: UnslopNemo 12B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "upstage/solar-pro-3": { - id: "upstage/solar-pro-3", - name: "Upstage: Solar Pro 3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-4.20": { - id: "x-ai/grok-4.20", - name: "xAI: Grok 4.20", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-4.3": { - id: "x-ai/grok-4.3", - name: "xAI: Grok 4.3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-build-0.1": { - id: "x-ai/grok-build-0.1", - name: "xAI: Grok Build 0.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "xiaomi/mimo-v2.5": { - id: "xiaomi/mimo-v2.5", - name: "Xiaomi: MiMo-V2.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "xiaomi/mimo-v2.5-pro": { - id: "xiaomi/mimo-v2.5-pro", - name: "Xiaomi: MiMo-V2.5-Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.0036, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.5": { - id: "z-ai/glm-4.5", - name: "Z.ai: GLM 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.5-air": { - id: "z-ai/glm-4.5-air", - name: "Z.ai: GLM 4.5 Air", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.13, - output: 0.85, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.5v": { - id: "z-ai/glm-4.5v", - name: "Z.ai: GLM 4.5V", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 1.8, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 65536, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "Z.ai: GLM 4.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.43, - output: 1.74, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.6v": { - id: "z-ai/glm-4.6v", - name: "Z.ai: GLM 4.6V", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 0.9, - cacheRead: 0.055, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "Z.ai: GLM 4.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.4, - output: 1.75, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.7-flash": { - id: "z-ai/glm-4.7-flash", - name: "Z.ai: GLM 4.7 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "Z.ai: GLM 5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 1.9, - cacheRead: 0.119, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "z-ai/glm-5-turbo": { - id: "z-ai/glm-5-turbo", - name: "Z.ai: GLM 5 Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "Z.ai: GLM 5.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.98, - output: 3.08, - cacheRead: 0.182, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "z-ai/glm-5.2": { - id: "z-ai/glm-5.2", - name: "Z.ai: GLM 5.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "~anthropic/claude-fable-latest": { - id: "~anthropic/claude-fable-latest", - name: "Anthropic: Claude Fable Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "~anthropic/claude-haiku-latest": { - id: "~anthropic/claude-haiku-latest", - name: "Anthropic Claude Haiku Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "~anthropic/claude-opus-latest": { - id: "~anthropic/claude-opus-latest", - name: "Anthropic: Claude Opus Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "~anthropic/claude-sonnet-latest": { - id: "~anthropic/claude-sonnet-latest", - name: "Anthropic Claude Sonnet Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "~google/gemini-flash-latest": { - id: "~google/gemini-flash-latest", - name: "Google Gemini Flash Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0.083333, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "~google/gemini-pro-latest": { - id: "~google/gemini-pro-latest", - name: "Google Gemini Pro Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "~moonshotai/kimi-latest": { - id: "~moonshotai/kimi-latest", - name: "MoonshotAI Kimi Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.68, - output: 3.41, - cacheRead: 0.34, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262142, - } satisfies Model<"openai-completions">, - "~openai/gpt-latest": { - id: "~openai/gpt-latest", - name: "OpenAI GPT Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "~openai/gpt-mini-latest": { - id: "~openai/gpt-mini-latest", - name: "OpenAI GPT Mini Latest", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - }, - "together": { - "MiniMaxAI/MiniMax-M2.7": { - id: "MiniMaxAI/MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "MiniMaxAI/MiniMax-M3": { - id: "MiniMaxAI/MiniMax-M3", - name: "MiniMax-M3", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 524288, - maxTokens: 250000, - } satisfies Model<"openai-completions">, - "Qwen/Qwen2.5-7B-Instruct-Turbo": { - id: "Qwen/Qwen2.5-7B-Instruct-Turbo", - name: "Qwen 2.5 7B Instruct Turbo", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput", - name: "Qwen3 235B A22B Instruct 2507 FP8", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5 397B A17B", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 130000, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3.5-9B": { - id: "Qwen/Qwen3.5-9B", - name: "Qwen3.5 9B", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.17, - output: 0.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3.6-Plus": { - id: "Qwen/Qwen3.6-Plus", - name: "Qwen3.6 Plus", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 500000, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3.7-Max": { - id: "Qwen/Qwen3.7-Max", - name: "Qwen3.7 Max", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 1.25, - output: 3.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 500000, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V4-Pro": { - id: "deepseek-ai/DeepSeek-V4-Pro", - name: "DeepSeek V4 Pro", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null}, - input: ["text"], - cost: { - input: 1.74, - output: 3.48, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 512000, - maxTokens: 384000, - } satisfies Model<"openai-completions">, - "essentialai/Rnj-1-Instruct": { - id: "essentialai/Rnj-1-Instruct", - name: "Rnj-1 Instruct", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "google/gemma-4-31B-it": { - id: "google/gemma-4-31B-it", - name: "Gemma 4 31B Instruct", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.39, - output: 0.97, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "meta-llama/Llama-3.3-70B-Instruct-Turbo": { - id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", - name: "Llama 3.3 70B", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.88, - output: 0.88, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.6": { - id: "moonshotai/Kimi-K2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 1.2, - output: 4.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131000, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.7-Code": { - id: "moonshotai/Kimi-K2.7-Code", - name: "Kimi K2.7 Code", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-ultra-550b-a55b": { - id: "nvidia/nemotron-3-ultra-550b-a55b", - name: "Nemotron 3 Ultra 550B A55B", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.6, - output: 3.6, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 512300, - maxTokens: 512300, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"openai"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null}, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"openai"}, - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null}, - input: ["text"], - cost: { - input: 0.05, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "vercel-ai-gateway": { - "alibaba/qwen-3-14b": { - id: "alibaba/qwen-3-14b", - name: "Qwen3-14B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.12, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 40960, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3-235b": { - id: "alibaba/qwen-3-235b", - name: "Qwen3 235B A22B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.22, - output: 0.88, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3-30b": { - id: "alibaba/qwen-3-30b", - name: "Qwen3-30B-A3B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.12, - output: 0.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 40960, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3-32b": { - id: "alibaba/qwen-3-32b", - name: "Qwen 3 32B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.16, - output: 0.64, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3.6-max-preview": { - id: "alibaba/qwen-3.6-max-preview", - name: "Qwen 3.6 Max Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.3, - output: 7.8, - cacheRead: 0.26, - cacheWrite: 1.625, - }, - contextWindow: 240000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-235b-a22b-thinking": { - id: "alibaba/qwen3-235b-a22b-thinking", - name: "Qwen3 VL 235B A22B Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder": { - id: "alibaba/qwen3-coder", - name: "Qwen3 Coder 480B A35B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder-30b-a3b": { - id: "alibaba/qwen3-coder-30b-a3b", - name: "Qwen 3 Coder 30B A3B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder-next": { - id: "alibaba/qwen3-coder-next", - name: "Qwen3 Coder Next", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder-plus": { - id: "alibaba/qwen3-coder-plus", - name: "Qwen3 Coder Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-max": { - id: "alibaba/qwen3-max", - name: "Qwen3 Max", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.2, - output: 6, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-max-preview": { - id: "alibaba/qwen3-max-preview", - name: "Qwen3 Max Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.2, - output: 6, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-max-thinking": { - id: "alibaba/qwen3-max-thinking", - name: "Qwen 3 Max Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 6, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-next-80b-a3b-instruct": { - id: "alibaba/qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-next-80b-a3b-thinking": { - id: "alibaba/qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-vl-thinking": { - id: "alibaba/qwen3-vl-thinking", - name: "Qwen3 VL 235B A22B Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.5-flash": { - id: "alibaba/qwen3.5-flash", - name: "Qwen 3.5 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.001, - cacheWrite: 0.125, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.5-plus": { - id: "alibaba/qwen3.5-plus", - name: "Qwen 3.5 Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2.4, - cacheRead: 0.04, - cacheWrite: 0.5, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.6-27b": { - id: "alibaba/qwen3.6-27b", - name: "Qwen 3.6 27B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.6-plus": { - id: "alibaba/qwen3.6-plus", - name: "Qwen 3.6 Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.1, - cacheWrite: 0.625, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.7-max": { - id: "alibaba/qwen3.7-max", - name: "Qwen 3.7 Max", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.25, - output: 3.75, - cacheRead: 0.25, - cacheWrite: 1.5625, - }, - contextWindow: 991000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.7-plus": { - id: "alibaba/qwen3.7-plus", - name: "Qwen 3.7 Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.08, - cacheWrite: 0.5, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Claude 3 Haiku", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude 3.5 Haiku", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude Haiku 4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude Opus 4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.8": { - id: "anthropic/claude-opus-4.8", - name: "Claude Opus 4.8", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "arcee-ai/trinity-large-preview": { - id: "arcee-ai/trinity-large-preview", - name: "Trinity Large Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Trinity Large Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 0.9, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262100, - maxTokens: 80000, - } satisfies Model<"anthropic-messages">, - "bytedance/seed-1.6": { - id: "bytedance/seed-1.6", - name: "Seed 1.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "cohere/command-a": { - id: "cohere/command-a", - name: "Command A", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek-R1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.35, - output: 5.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3": { - id: "deepseek/deepseek-v3", - name: "DeepSeek V3 0324", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.27, - output: 1.12, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek V3.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.56, - output: 1.68, - cacheRead: 0.28, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.27, - output: 1, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.28, - output: 0.42, - cacheRead: 0.028, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.2-thinking": { - id: "deepseek/deepseek-v3.2-thinking", - name: "DeepSeek V3.2 Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.62, - output: 1.85, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v4-flash": { - id: "deepseek/deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v4-pro": { - id: "deepseek/deepseek-v4-pro", - name: "DeepSeek V4 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.0036, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "google/gemini-3-flash": { - id: "google/gemini-3-flash", - name: "Gemini 3 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3-pro-preview": { - id: "google/gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3.1-flash-lite": { - id: "google/gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3.5-flash": { - id: "google/gemini-3.5-flash", - name: "Gemini 3.5 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.5, - output: 9, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma 4 31B IT", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Mercury 2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 0.75, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "inception/mercury-coder-small": { - id: "inception/mercury-coder-small", - name: "Mercury Coder Small Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "kwaipilot/kat-coder-pro-v2": { - id: "kwaipilot/kat-coder-pro-v2", - name: "Kat Coder Pro V2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "meituan/longcat-flash-chat": { - id: "meituan/longcat-flash-chat", - name: "LongCat Flash Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.1-70b": { - id: "meta/llama-3.1-70b", - name: "Llama 3.1 70B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.1-8b": { - id: "meta/llama-3.1-8b", - name: "Llama 3.1 8B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 0.22, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.2-11b": { - id: "meta/llama-3.2-11b", - name: "Llama 3.2 11B Vision Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.16, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.2-90b": { - id: "meta/llama-3.2-90b", - name: "Llama 3.2 90B Vision Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.3-70b": { - id: "meta/llama-3.3-70b", - name: "Llama 3.3 70B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-4-maverick": { - id: "meta/llama-4-maverick", - name: "Llama 4 Maverick 17B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.24, - output: 0.97, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-4-scout": { - id: "meta/llama-4-scout", - name: "Llama 4 Scout 17B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.17, - output: 0.66, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax M2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 205000, - maxTokens: 205000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.1-lightning": { - id: "minimax/minimax-m2.1-lightning", - name: "MiniMax M2.1 Lightning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 2.4, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.5-highspeed": { - id: "minimax/minimax-m2.5-highspeed", - name: "MiniMax M2.5 High Speed", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax M2.7", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.7-highspeed": { - id: "minimax/minimax-m2.7-highspeed", - name: "MiniMax M2.7 High Speed", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m3": { - id: "minimax/minimax-m3", - name: "MiniMax M3", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 1000000, - } satisfies Model<"anthropic-messages">, - "mistral/codestral": { - id: "mistral/codestral", - name: "Mistral Codestral", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.9, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/devstral-2": { - id: "mistral/devstral-2", - name: "Devstral 2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "mistral/devstral-small": { - id: "mistral/devstral-small", - name: "Devstral Small 1.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "mistral/devstral-small-2": { - id: "mistral/devstral-small-2", - name: "Devstral Small 2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "mistral/ministral-3b": { - id: "mistral/ministral-3b", - name: "Ministral 3B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/ministral-8b": { - id: "mistral/ministral-8b", - name: "Ministral 8B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/mistral-medium": { - id: "mistral/mistral-medium", - name: "Mistral Medium 3.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "mistral/mistral-medium-3.5": { - id: "mistral/mistral-medium-3.5", - name: "Mistral Medium Latest", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "mistral/mistral-nemo": { - id: "mistral/mistral-nemo", - name: "Mistral Nemo 12B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "mistral/mistral-small": { - id: "mistral/mistral-small", - name: "Mistral Small", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/pixtral-12b": { - id: "mistral/pixtral-12b", - name: "Pixtral 12B 2409", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/pixtral-large": { - id: "mistral/pixtral-large", - name: "Pixtral Large", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "Kimi K2 Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.57, - output: 2.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2.7-code": { - id: "moonshotai/kimi-k2.7-code", - name: "Kimi K2.7 Code", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2.7-code-highspeed": { - id: "moonshotai/kimi-k2.7-code-highspeed", - name: "Kimi K2.7 Code High Speed", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.9, - output: 8, - cacheRead: 0.38, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "NVIDIA Nemotron 3 Super 120B A12B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.65, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "nvidia/nemotron-3-ultra-550b-a55b": { - id: "nvidia/nemotron-3-ultra-550b-a55b", - name: "Nemotron 3 Ultra", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.12, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65000, - } satisfies Model<"anthropic-messages">, - "nvidia/nemotron-nano-12b-v2-vl": { - id: "nvidia/nemotron-nano-12b-v2-vl", - name: "Nvidia Nemotron Nano 12B V2 VL", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "Nvidia Nemotron Nano 9B V2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.23, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4 Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1 mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT-4.1 nano", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT 5 Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5-Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 nano", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5 pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 272000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT 5.1 Codex Max", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT 5.1 Codex Mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-instant": { - id: "openai/gpt-5.1-instant", - name: "GPT-5.1 Instant", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-thinking": { - id: "openai/gpt-5.1-thinking", - name: "GPT 5.1 Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT 5.2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT 5.2 Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT 5.2 Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT 5.2 ", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "GPT-5.3 Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT 5.3 Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT 5.4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT 5.4 Mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT 5.4 Nano", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT 5.4 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "GPT 5.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.5-pro": { - id: "openai/gpt-5.5-pro", - name: "GPT 5.5 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","off":null,"minimal":null,"low":null}, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.35, - output: 0.75, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.05, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.037, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "openai/o1": { - id: "openai/o1", - name: "o1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3": { - id: "openai/o3", - name: "o3", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "o3-deep-research", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "o3-mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "o3 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4-mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "perplexity/sonar": { - id: "perplexity/sonar", - name: "Sonar", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 127000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "perplexity/sonar-pro": { - id: "perplexity/sonar-pro", - name: "Sonar Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "StepFun 3.5 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.09, - output: 0.3, - cacheRead: 0, - cacheWrite: 0.02, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "stepfun/step-3.7-flash": { - id: "stepfun/step-3.7-flash", - name: "Step 3.7 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.15, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.1-fast-non-reasoning": { - id: "xai/grok-4.1-fast-non-reasoning", - name: "Grok 4.1 Fast Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 1000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.1-fast-reasoning": { - id: "xai/grok-4.1-fast-reasoning", - name: "Grok 4.1 Fast Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 1000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-multi-agent": { - id: "xai/grok-4.20-multi-agent", - name: "Grok 4.20 Multi-Agent", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-multi-agent-beta": { - id: "xai/grok-4.20-multi-agent-beta", - name: "Grok 4.20 Multi Agent Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-non-reasoning": { - id: "xai/grok-4.20-non-reasoning", - name: "Grok 4.20 Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-non-reasoning-beta": { - id: "xai/grok-4.20-non-reasoning-beta", - name: "Grok 4.20 Beta Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-reasoning": { - id: "xai/grok-4.20-reasoning", - name: "Grok 4.20 Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-reasoning-beta": { - id: "xai/grok-4.20-reasoning-beta", - name: "Grok 4.20 Beta Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.3": { - id: "xai/grok-4.3", - name: "Grok 4.3", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 1000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-build-0.1": { - id: "xai/grok-build-0.1", - name: "Grok Build 0.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "MiMo V2 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "MiMo V2 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "xiaomi/mimo-v2.5": { - id: "xiaomi/mimo-v2.5", - name: "MiMo M2.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.0028, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "xiaomi/mimo-v2.5-pro": { - id: "xiaomi/mimo-v2.5-pro", - name: "MiMo V2.5 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.0036, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.5": { - id: "zai/glm-4.5", - name: "GLM-4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 96000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.5-air": { - id: "zai/glm-4.5-air", - name: "GLM 4.5 Air", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.2, - output: 1.1, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 96000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.5v": { - id: "zai/glm-4.5v", - name: "GLM 4.5V", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 1.8, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 66000, - maxTokens: 16000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.6": { - id: "zai/glm-4.6", - name: "GLM 4.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 96000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.6v": { - id: "zai/glm-4.6v", - name: "GLM-4.6V", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 0.9, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 24000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.6v-flash": { - id: "zai/glm-4.6v-flash", - name: "GLM-4.6V-Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 24000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.7": { - id: "zai/glm-4.7", - name: "GLM 4.7", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 2.25, - output: 2.75, - cacheRead: 2.25, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 40000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.7-flash": { - id: "zai/glm-4.7-flash", - name: "GLM 4.7 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.7-flashx": { - id: "zai/glm-4.7-flashx", - name: "GLM 4.7 FlashX", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "zai/glm-5": { - id: "zai/glm-5", - name: "GLM 5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "zai/glm-5-turbo": { - id: "zai/glm-5-turbo", - name: "GLM 5 Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "zai/glm-5.1": { - id: "zai/glm-5.1", - name: "GLM 5.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "zai/glm-5.2": { - id: "zai/glm-5.2", - name: "GLM 5.2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.5, - output: 4.5, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "zai/glm-5v-turbo": { - id: "zai/glm-5v-turbo", - name: "GLM 5V Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, - "xai": { - "grok-3": { - id: "grok-3", - name: "Grok 3", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-fast": { - id: "grok-3-fast", - name: "Grok 3 Fast", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 5, - output: 25, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-4.20-0309-non-reasoning": { - id: "grok-4.20-0309-non-reasoning", - name: "Grok 4.20 (Non-Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4.20-0309-reasoning": { - id: "grok-4.20-0309-reasoning", - name: "Grok 4.20 (Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4.3": { - id: "grok-4.3", - name: "Grok 4.3", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-build-0.1": { - id: "grok-build-0.1", - name: "Grok Build 0.1", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 1.5, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - }, - "xiaomi": { - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro-ultraspeed": { - id: "mimo-v2.5-pro-ultraspeed", - name: "MiMo-V2.5-Pro-UltraSpeed", - api: "openai-completions", - provider: "xiaomi", - baseUrl: "https://api.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.305, - output: 2.61, - cacheRead: 0.0108, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "xiaomi-token-plan-ams": { - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro-ultraspeed": { - id: "mimo-v2.5-pro-ultraspeed", - name: "MiMo-V2.5-Pro-UltraSpeed", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.305, - output: 2.61, - cacheRead: 0.0108, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "xiaomi-token-plan-cn": { - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro-ultraspeed": { - id: "mimo-v2.5-pro-ultraspeed", - name: "MiMo-V2.5-Pro-UltraSpeed", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.305, - output: 2.61, - cacheRead: 0.0108, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "xiaomi-token-plan-sgp": { - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "mimo-v2.5-pro-ultraspeed": { - id: "mimo-v2.5-pro-ultraspeed", - name: "MiMo-V2.5-Pro-UltraSpeed", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 1.305, - output: 2.61, - cacheRead: 0.0108, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "zai": { - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.2": { - id: "glm-5.2", - name: "GLM-5.2", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","supportsReasoningEffort":true,"zaiToolStream":true}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "GLM-5V-Turbo", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "zai-coding-cn": { - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.2": { - id: "glm-5.2", - name: "GLM-5.2", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","supportsReasoningEffort":true,"zaiToolStream":true}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "GLM-5V-Turbo", - api: "openai-completions", - provider: "zai-coding-cn", - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, + "amazon-bedrock": AMAZON_BEDROCK_MODELS, + "ant-ling": ANT_LING_MODELS, + "anthropic": ANTHROPIC_MODELS, + "azure-openai-responses": AZURE_OPENAI_RESPONSES_MODELS, + "cerebras": CEREBRAS_MODELS, + "cloudflare-ai-gateway": CLOUDFLARE_AI_GATEWAY_MODELS, + "cloudflare-workers-ai": CLOUDFLARE_WORKERS_AI_MODELS, + "deepseek": DEEPSEEK_MODELS, + "fireworks": FIREWORKS_MODELS, + "github-copilot": GITHUB_COPILOT_MODELS, + "google": GOOGLE_MODELS, + "google-vertex": GOOGLE_VERTEX_MODELS, + "groq": GROQ_MODELS, + "huggingface": HUGGINGFACE_MODELS, + "kimi-coding": KIMI_CODING_MODELS, + "minimax": MINIMAX_MODELS, + "minimax-cn": MINIMAX_CN_MODELS, + "mistral": MISTRAL_MODELS, + "moonshotai": MOONSHOTAI_MODELS, + "moonshotai-cn": MOONSHOTAI_CN_MODELS, + "nvidia": NVIDIA_MODELS, + "openai": OPENAI_MODELS, + "openai-codex": OPENAI_CODEX_MODELS, + "opencode": OPENCODE_MODELS, + "opencode-go": OPENCODE_GO_MODELS, + "openrouter": OPENROUTER_MODELS, + "together": TOGETHER_MODELS, + "vercel-ai-gateway": VERCEL_AI_GATEWAY_MODELS, + "xai": XAI_MODELS, + "xiaomi": XIAOMI_MODELS, + "xiaomi-token-plan-ams": XIAOMI_TOKEN_PLAN_AMS_MODELS, + "xiaomi-token-plan-cn": XIAOMI_TOKEN_PLAN_CN_MODELS, + "xiaomi-token-plan-sgp": XIAOMI_TOKEN_PLAN_SGP_MODELS, + "zai": ZAI_MODELS, + "zai-coding-cn": ZAI_CODING_CN_MODELS, } as const; diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 9d9fa519..f9cf27d3 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -1,39 +1,385 @@ -import { MODELS } from "./models.generated.ts"; -import type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from "./types.ts"; +import { lazyStream } from "./api/lazy.ts"; +import { defaultProviderAuthContext as defaultAuthContext } from "./auth/context.ts"; +import { InMemoryCredentialStore } from "./auth/credential-store.ts"; +import { ModelsError, resolveProviderAuth } from "./auth/resolve.ts"; +import type { AuthContext, AuthResult, CredentialStore, ProviderAuth } from "./auth/types.ts"; +import type { + Api, + ApiStreamOptions, + AssistantMessage, + AssistantMessageEventStream, + Context, + Model, + ModelThinkingLevel, + ProviderHeaders, + ProviderStreams, + SimpleStreamOptions, + StreamOptions, + Usage, +} from "./types.ts"; -const modelRegistry: Map>> = new Map(); +export { type AuthModel, ModelsError, type ModelsErrorCode } from "./auth/resolve.ts"; -// Initialize registry from MODELS on module load -for (const [provider, models] of Object.entries(MODELS)) { - const providerModels = new Map>(); - for (const [id, model] of Object.entries(models)) { - providerModels.set(id, model as Model); +/** + * A provider is the concrete runtime unit. It owns id/name/base metadata, + * auth methods, model listing, and stream behavior. + * + * `TApi` lets concrete provider factories declare which APIs their models + * use (e.g. `openaiProvider(): Provider<"openai-responses" | "openai-completions">`), + * giving typed model lists to direct factory users. Inside a `Models` + * collection providers are held as `Provider`. + */ +export interface Provider { + readonly id: string; + readonly name: string; + + readonly baseUrl?: string; + readonly headers?: ProviderHeaders; + + /** + * Required: at least one of `apiKey`/`oauth`. Every provider has auth + * semantics — even providers with only ambient credentials (env vars, AWS + * profiles, ADC files) and keyless local servers provide `apiKey` auth + * whose `resolve()` reports whether the provider is configured. + * `Models.getAuth()` returns undefined when the provider is unconfigured. + */ + readonly auth: ProviderAuth; + + /** + * Current known models, sync. Static providers return their catalog; + * dynamic providers return the list as of the last `refreshModels()` + * (empty before the first). Must not throw; `Models` treats a throwing + * implementation as having no models. + */ + getModels(): readonly Model[]; + + /** + * Dynamic providers only: fetch and update the model list. Side-effect-free + * discovery (no loading/downloading); provider-specific model lifecycle + * belongs in app commands. Concurrent calls share one in-flight fetch. + * May reject (network); on rejection the model list stays at its last-known + * state and a later call retries. + */ + refreshModels?(): Promise; + + stream( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): AssistantMessageEventStream; + + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; +} + +/** + * Runtime collection of providers plus auth application and stream + * convenience. Providers own stream behavior; `Models` resolves auth and + * delegates each request to the provider that owns the model. + */ +export interface Models { + getProviders(): readonly Provider[]; + getProvider(id: string): Provider | undefined; + + /** + * Sync read of last-known models from one provider or all providers. + * Best-effort: a provider whose `getModels()` throws yields no models. + */ + getModels(provider?: string): readonly Model[]; + + /** + * Sync runtime model lookup against last-known lists. Dynamic model lists + * are typed as `Model`; narrow with the `hasApi()` type guard. + */ + getModel(provider: string, id: string): Model | undefined; + + /** + * Ask dynamic providers to re-fetch their model lists. With a provider id, + * rejects with `ModelsError` ("model_source") on that provider's fetch + * failure; without one, refreshes all providers concurrently best-effort. + * Static providers (no `refreshModels`) are no-ops. + */ + refresh(provider?: string): Promise; + + /** + * Resolve request auth for a model. Includes a source label for status UI. + * Resolves `undefined` when the provider is unknown or unconfigured. + * Rejects with `ModelsError`: code "oauth" when a token refresh fails (the + * stored credential is preserved for retry; re-login fixes it), code "auth" + * when api-key resolution or the credential store fails. Request paths + * surface rejections as stream errors; status/availability UIs catch them + * 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; +} + +export interface CreateModelsOptions { + credentials?: CredentialStore; + authContext?: AuthContext; +} + +class ModelsImpl implements MutableModels { + private providers = new Map(); + private credentials: CredentialStore; + private authContext: AuthContext; + + constructor(options?: CreateModelsOptions) { + this.credentials = options?.credentials ?? new InMemoryCredentialStore(); + this.authContext = options?.authContext ?? defaultAuthContext(); + } + + setProvider(provider: Provider): void { + this.providers.set(provider.id, provider); + } + + deleteProvider(id: string): void { + this.providers.delete(id); + } + + clearProviders(): void { + this.providers.clear(); + } + + getProviders(): readonly Provider[] { + return Array.from(this.providers.values()); + } + + getProvider(id: string): Provider | undefined { + return this.providers.get(id); + } + + getModels(provider?: string): readonly Model[] { + if (provider !== undefined) { + const entry = this.providers.get(provider); + if (!entry) return []; + try { + return entry.getModels(); + } catch { + return []; + } + } + + const models: Model[] = []; + for (const entry of this.providers.values()) { + try { + models.push(...entry.getModels()); + } catch { + // Best-effort: ill-behaved providers yield no models. + } + } + return models; + } + + getModel(provider: string, id: string): Model | undefined { + return this.getModels(provider).find((model) => model.id === id); + } + + async refresh(provider?: string): Promise { + if (provider !== undefined) { + const entry = this.providers.get(provider); + if (!entry?.refreshModels) return; + try { + await entry.refreshModels(); + } catch (error) { + if (error instanceof ModelsError) throw error; + throw new ModelsError("model_source", `Model refresh failed for ${provider}`, { cause: error }); + } + return; + } + + // Cannot reject: the async mapper turns even sync throws from ill-behaved + // providers into rejections, and allSettled captures all of them. + await Promise.allSettled(Array.from(this.providers.values(), async (entry) => entry.refreshModels?.())); + } + + async getAuth(model: Model): Promise { + const provider = this.providers.get(model.provider); + if (!provider) return undefined; + return resolveProviderAuth(provider, model, this.credentials, this.authContext); + } + + private requireProvider(model: Model): Provider { + const provider = this.providers.get(model.provider); + if (!provider) { + throw new ModelsError("provider", `Unknown provider: ${model.provider}`); + } + return provider; + } + + private async applyAuth( + model: Model, + options: TOptions | undefined, + ): Promise<{ requestModel: Model; requestOptions: TOptions | undefined }> { + const resolution = await resolveProviderAuth( + this.requireProvider(model), + model, + this.credentials, + this.authContext, + { + apiKey: options?.apiKey, + env: options?.env, + }, + ); + const auth = resolution?.auth; + if (!auth) return { requestModel: model, requestOptions: options }; + + const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model; + + // Explicit request options win per-field; headers/env merge per key. + const apiKey = options?.apiKey ?? auth.apiKey; + const headers = auth.headers || options?.headers ? { ...auth.headers, ...options?.headers } : undefined; + const env = resolution.env || options?.env ? { ...(resolution.env ?? {}), ...(options?.env ?? {}) } : undefined; + const requestOptions = { ...options, apiKey, headers, env } as TOptions; + + return { requestModel, requestOptions }; + } + + stream( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): AssistantMessageEventStream { + return lazyStream(model, async () => { + const provider = this.requireProvider(model); + const { requestModel, requestOptions } = await this.applyAuth(model, options as StreamOptions | undefined); + return provider.stream(requestModel as Model, context, requestOptions as ApiStreamOptions); + }); + } + + async complete( + model: Model, + context: Context, + options?: ApiStreamOptions, + ): Promise { + return this.stream(model, context, options).result(); + } + + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream { + return lazyStream(model, async () => { + const provider = this.requireProvider(model); + const { requestModel, requestOptions } = await this.applyAuth(model, options); + return provider.streamSimple(requestModel, context, requestOptions); + }); + } + + async completeSimple(model: Model, context: Context, options?: SimpleStreamOptions): Promise { + return this.streamSimple(model, context, options).result(); } - modelRegistry.set(provider, providerModels); } -type ModelApi< - TProvider extends KnownProvider, - TModelId extends keyof (typeof MODELS)[TProvider], -> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never; - -export function getModel( - provider: TProvider, - modelId: TModelId, -): Model> { - const providerModels = modelRegistry.get(provider); - return providerModels?.get(modelId as string) as Model>; +export function createModels(options?: CreateModelsOptions): MutableModels { + return new ModelsImpl(options); } -export function getProviders(): KnownProvider[] { - return Array.from(modelRegistry.keys()) as KnownProvider[]; +export interface CreateProviderOptions { + id: string; + /** Display name. Default: `id`. */ + name?: string; + baseUrl?: string; + headers?: ProviderHeaders; + /** Required — every provider has auth semantics, even ambient/keyless ones. */ + auth: ProviderAuth; + /** Initial model list (empty for purely dynamic providers). */ + models: readonly Model[]; + /** + * Dynamic providers: fetch the current list. Stored on success; concurrent + * calls share one in-flight fetch. May reject: the stored list then stays + * at its last-known state, the rejection propagates to the caller of + * `refreshModels()` (wrapped as ModelsError "model_source" by + * `Models.refresh(provider)`), and a later call retries. + */ + refreshModels?: () => Promise[]>; + /** Single implementation, or map keyed by `model.api` for mixed-API providers. */ + api: ProviderStreams | Partial>; } -export function getModels( - provider: TProvider, -): Model>[] { - const models = modelRegistry.get(provider); - return models ? (Array.from(models.values()) as Model>[]) : []; +/** + * Builds a provider from parts. Built-in provider factories and models.json + * custom providers both go through this. A single `api` streams all models; + * an `api` map dispatches on `model.api`, and a model whose api has no entry + * produces a stream error. + */ +export function createProvider(input: CreateProviderOptions): Provider { + let models = input.models; + let inflightRefresh: Promise | undefined; + const refreshModels = input.refreshModels; + const single = + typeof (input.api as ProviderStreams).stream === "function" ? (input.api as ProviderStreams) : undefined; + const byApi = single ? undefined : (input.api as Partial>); + + const apiFor = (model: Model): ProviderStreams | undefined => single ?? byApi?.[model.api]; + + const dispatch = ( + model: Model, + run: (streams: ProviderStreams) => AssistantMessageEventStream, + ): AssistantMessageEventStream => { + const streams = apiFor(model); + if (!streams) { + return lazyStream(model, async () => { + throw new ModelsError("stream", `Provider ${input.id} has no API implementation for "${model.api}"`); + }); + } + return run(streams); + }; + + return { + id: input.id, + name: input.name ?? input.id, + baseUrl: input.baseUrl, + headers: input.headers, + auth: input.auth, + getModels: () => models, + refreshModels: refreshModels + ? () => { + inflightRefresh ??= (async () => { + try { + models = await refreshModels(); + } finally { + inflightRefresh = undefined; + } + })(); + return inflightRefresh; + } + : undefined, + stream: (model, context, options) => dispatch(model, (streams) => streams.stream(model, context, options)), + streamSimple: (model, context, options) => + dispatch(model, (streams) => streams.streamSimple(model, context, options)), + }; +} + +/** + * Runtime-checked narrowing for dynamically looked-up models: + * + * ```ts + * const model = models.getModel("anthropic", "claude-opus-4-7"); + * if (model && hasApi(model, "anthropic-messages")) { + * // model: Model<"anthropic-messages">, stream options fully typed + * } + * ``` + */ +export function hasApi(model: Model, api: TApi): model is Model { + return model.api === api; } export function calculateCost(model: Model, usage: Usage): Usage["cost"] { diff --git a/packages/ai/src/providers/all.ts b/packages/ai/src/providers/all.ts new file mode 100644 index 00000000..85ba0301 --- /dev/null +++ b/packages/ai/src/providers/all.ts @@ -0,0 +1,131 @@ +import { createImagesModels, type ImagesProvider, type MutableImagesModels } from "../images-models.ts"; +import { MODELS } from "../models.generated.ts"; +import { type CreateModelsOptions, createModels, type MutableModels, type Provider } from "../models.ts"; +import type { Api, KnownProvider, Model } from "../types.ts"; +import { amazonBedrockProvider } from "./amazon-bedrock.ts"; +import { antLingProvider } from "./ant-ling.ts"; +import { anthropicProvider } from "./anthropic.ts"; +import { azureOpenAIResponsesProvider } from "./azure-openai-responses.ts"; +import { cerebrasProvider } from "./cerebras.ts"; +import { cloudflareAIGatewayProvider } from "./cloudflare-ai-gateway.ts"; +import { cloudflareWorkersAIProvider } from "./cloudflare-workers-ai.ts"; +import { deepseekProvider } from "./deepseek.ts"; +import { fireworksProvider } from "./fireworks.ts"; +import { githubCopilotProvider } from "./github-copilot.ts"; +import { googleProvider } from "./google.ts"; +import { googleVertexProvider } from "./google-vertex.ts"; +import { groqProvider } from "./groq.ts"; +import { huggingfaceProvider } from "./huggingface.ts"; +import { kimiCodingProvider } from "./kimi-coding.ts"; +import { minimaxProvider } from "./minimax.ts"; +import { minimaxCnProvider } from "./minimax-cn.ts"; +import { mistralProvider } from "./mistral.ts"; +import { moonshotaiProvider } from "./moonshotai.ts"; +import { moonshotaiCnProvider } from "./moonshotai-cn.ts"; +import { nvidiaProvider } from "./nvidia.ts"; +import { openaiProvider } from "./openai.ts"; +import { openaiCodexProvider } from "./openai-codex.ts"; +import { opencodeProvider } from "./opencode.ts"; +import { opencodeGoProvider } from "./opencode-go.ts"; +import { openrouterProvider } from "./openrouter.ts"; +import { openrouterImagesProvider } from "./openrouter-images.ts"; +import { togetherProvider } from "./together.ts"; +import { vercelAIGatewayProvider } from "./vercel-ai-gateway.ts"; +import { xaiProvider } from "./xai.ts"; +import { xiaomiProvider } from "./xiaomi.ts"; +import { xiaomiTokenPlanAmsProvider } from "./xiaomi-token-plan-ams.ts"; +import { xiaomiTokenPlanCnProvider } from "./xiaomi-token-plan-cn.ts"; +import { xiaomiTokenPlanSgpProvider } from "./xiaomi-token-plan-sgp.ts"; +import { zaiProvider } from "./zai.ts"; +import { zaiCodingCnProvider } from "./zai-coding-cn.ts"; + +type BuiltinModelApi< + TProvider extends KnownProvider, + TModelId extends keyof (typeof MODELS)[TProvider], +> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never; + +/** Typed read of the generated built-in catalog. */ +export function getBuiltinModel( + provider: TProvider, + modelId: TModelId, +): Model> { + const models = MODELS[provider] as Record> | undefined; + return models?.[modelId as string] as Model>; +} + +export function getBuiltinProviders(): KnownProvider[] { + return Object.keys(MODELS) as KnownProvider[]; +} + +export function getBuiltinModels( + provider: TProvider, +): Model>[] { + const models = MODELS[provider] as Record> | undefined; + return models + ? (Object.values(models) as Model>[]) + : []; +} + +/** All built-in providers, freshly constructed. */ +export function builtinProviders(): Provider[] { + return [ + amazonBedrockProvider(), + antLingProvider(), + anthropicProvider(), + azureOpenAIResponsesProvider(), + cerebrasProvider(), + cloudflareAIGatewayProvider(), + cloudflareWorkersAIProvider(), + deepseekProvider(), + fireworksProvider(), + githubCopilotProvider(), + googleProvider(), + googleVertexProvider(), + groqProvider(), + huggingfaceProvider(), + kimiCodingProvider(), + minimaxProvider(), + minimaxCnProvider(), + mistralProvider(), + moonshotaiProvider(), + moonshotaiCnProvider(), + nvidiaProvider(), + openaiProvider(), + openaiCodexProvider(), + opencodeProvider(), + opencodeGoProvider(), + openrouterProvider(), + togetherProvider(), + vercelAIGatewayProvider(), + xaiProvider(), + xiaomiProvider(), + xiaomiTokenPlanAmsProvider(), + xiaomiTokenPlanCnProvider(), + xiaomiTokenPlanSgpProvider(), + zaiProvider(), + zaiCodingCnProvider(), + ]; +} + +/** A `Models` collection with every built-in provider registered. */ +export function builtinModels(options?: CreateModelsOptions): MutableModels { + const models = createModels(options); + for (const provider of builtinProviders()) { + models.setProvider(provider); + } + return models; +} + +/** All built-in image-generation providers, freshly constructed. */ +export function builtinImagesProviders(): ImagesProvider[] { + return [openrouterImagesProvider()]; +} + +/** An `ImagesModels` collection with every built-in image-generation provider registered. */ +export function builtinImagesModels(options?: CreateModelsOptions): MutableImagesModels { + const models = createImagesModels(options); + for (const provider of builtinImagesProviders()) { + models.setProvider(provider); + } + return models; +} diff --git a/packages/ai/src/providers/amazon-bedrock.models.ts b/packages/ai/src/providers/amazon-bedrock.models.ts new file mode 100644 index 00000000..37c21dce --- /dev/null +++ b/packages/ai/src/providers/amazon-bedrock.models.ts @@ -0,0 +1,1677 @@ +// 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 AMAZON_BEDROCK_MODELS = { + "amazon.nova-2-lite-v1:0": { + id: "amazon.nova-2-lite-v1:0", + name: "Nova 2 Lite", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.33, + output: 2.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "amazon.nova-lite-v1:0": { + id: "amazon.nova-lite-v1:0", + name: "Nova Lite", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.06, + output: 0.24, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 300000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "amazon.nova-micro-v1:0": { + id: "amazon.nova-micro-v1:0", + name: "Nova Micro", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.035, + output: 0.14, + cacheRead: 0.00875, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "amazon.nova-pro-v1:0": { + id: "amazon.nova-pro-v1:0", + name: "Nova Pro", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 300000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-1-20250805-v1:0": { + id: "anthropic.claude-opus-4-1-20250805-v1:0", + name: "Claude Opus 4.1", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-5-20251101-v1:0": { + id: "anthropic.claude-opus-4-5-20251101-v1:0", + name: "Claude Opus 4.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-6-v1": { + id: "anthropic.claude-opus-4-6-v1", + name: "Claude Opus 4.6", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-7": { + id: "anthropic.claude-opus-4-7", + name: "Claude Opus 4.7", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-8": { + id: "anthropic.claude-opus-4-8", + name: "Claude Opus 4.8", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-sonnet-4-6": { + id: "anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "au.anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5 (AU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-opus-4-6-v1": { + id: "au.anthropic.claude-opus-4-6-v1", + name: "AU Anthropic Claude Opus 4.6", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 16.5, + output: 82.5, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-opus-4-8": { + id: "au.anthropic.claude-opus-4-8", + name: "Claude Opus 4.8 (AU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (AU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-sonnet-4-6": { + id: "au.anthropic.claude-sonnet-4-6", + name: "AU Anthropic Claude Sonnet 4.6", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3.3, + output: 16.5, + cacheRead: 0.33, + cacheWrite: 4.125, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "deepseek.r1-v1:0": { + id: "deepseek.r1-v1:0", + name: "DeepSeek-R1", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 1.35, + output: 5.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32768, + } satisfies Model<"bedrock-converse-stream">, + "deepseek.v3-v1:0": { + id: "deepseek.v3-v1:0", + name: "DeepSeek-V3.1", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.58, + output: 1.68, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 81920, + } satisfies Model<"bedrock-converse-stream">, + "deepseek.v3.2": { + id: "deepseek.v3.2", + name: "DeepSeek-V3.2", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.62, + output: 1.85, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 81920, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-fable-5": { + id: "eu.anthropic.claude-fable-5", + name: "Claude Fable 5 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 11, + output: 55, + cacheRead: 1.1, + cacheWrite: 13.75, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-opus-4-5-20251101-v1:0": { + id: "eu.anthropic.claude-opus-4-5-20251101-v1:0", + name: "Claude Opus 4.5 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-opus-4-6-v1": { + id: "eu.anthropic.claude-opus-4-6-v1", + name: "Claude Opus 4.6 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5.5, + output: 27.5, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-opus-4-7": { + id: "eu.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5.5, + output: 27.5, + cacheRead: 0.55, + cacheWrite: 6.875, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-opus-4-8": { + id: "eu.anthropic.claude-opus-4-8", + name: "Claude Opus 4.8 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5.5, + output: 27.5, + cacheRead: 0.55, + cacheWrite: 6.875, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3.3, + output: 16.5, + cacheRead: 0.33, + cacheWrite: 4.125, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-sonnet-4-6": { + id: "eu.anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3.3, + output: 16.5, + cacheRead: 0.33, + cacheWrite: 4.125, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-fable-5": { + id: "global.anthropic.claude-fable-5", + name: "Claude Fable 5 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "global.anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-opus-4-5-20251101-v1:0": { + id: "global.anthropic.claude-opus-4-5-20251101-v1:0", + name: "Claude Opus 4.5 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-opus-4-6-v1": { + id: "global.anthropic.claude-opus-4-6-v1", + name: "Claude Opus 4.6 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-opus-4-7": { + id: "global.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-opus-4-8": { + id: "global.anthropic.claude-opus-4-8", + name: "Claude Opus 4.8 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-sonnet-4-6": { + id: "global.anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "google.gemma-3-27b-it": { + id: "google.gemma-3-27b-it", + name: "Google Gemma 3 27B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.12, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "google.gemma-3-4b-it": { + id: "google.gemma-3-4b-it", + name: "Gemma 3 4B IT", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.04, + output: 0.08, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-opus-4-7": { + id: "jp.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-opus-4-8": { + id: "jp.anthropic.claude-opus-4-8", + name: "Claude Opus 4.8 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-sonnet-4-6": { + id: "jp.anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "meta.llama3-1-70b-instruct-v1:0": { + id: "meta.llama3-1-70b-instruct-v1:0", + name: "Llama 3.1 70B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.72, + output: 0.72, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "meta.llama3-1-8b-instruct-v1:0": { + id: "meta.llama3-1-8b-instruct-v1:0", + name: "Llama 3.1 8B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.22, + output: 0.22, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "meta.llama3-3-70b-instruct-v1:0": { + id: "meta.llama3-3-70b-instruct-v1:0", + name: "Llama 3.3 70B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.72, + output: 0.72, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "meta.llama4-maverick-17b-instruct-v1:0": { + id: "meta.llama4-maverick-17b-instruct-v1:0", + name: "Llama 4 Maverick 17B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.24, + output: 0.97, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "meta.llama4-scout-17b-instruct-v1:0": { + id: "meta.llama4-scout-17b-instruct-v1:0", + name: "Llama 4 Scout 17B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.66, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 3500000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "minimax.minimax-m2": { + id: "minimax.minimax-m2", + name: "MiniMax M2", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204608, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "minimax.minimax-m2.1": { + id: "minimax.minimax-m2.1", + name: "MiniMax M2.1", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "minimax.minimax-m2.5": { + id: "minimax.minimax-m2.5", + name: "MiniMax M2.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 196608, + maxTokens: 98304, + } satisfies Model<"bedrock-converse-stream">, + "mistral.devstral-2-123b": { + id: "mistral.devstral-2-123b", + name: "Devstral 2 123B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "mistral.magistral-small-2509": { + id: "mistral.magistral-small-2509", + name: "Magistral Small 1.2", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 40000, + } satisfies Model<"bedrock-converse-stream">, + "mistral.ministral-3-14b-instruct": { + id: "mistral.ministral-3-14b-instruct", + name: "Ministral 14B 3.0", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.2, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "mistral.ministral-3-3b-instruct": { + id: "mistral.ministral-3-3b-instruct", + name: "Ministral 3 3B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "mistral.ministral-3-8b-instruct": { + id: "mistral.ministral-3-8b-instruct", + name: "Ministral 3 8B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "mistral.mistral-large-3-675b-instruct": { + id: "mistral.mistral-large-3-675b-instruct", + name: "Mistral Large 3", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "mistral.pixtral-large-2502-v1:0": { + id: "mistral.pixtral-large-2502-v1:0", + name: "Pixtral Large (25.02)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "mistral.voxtral-mini-3b-2507": { + id: "mistral.voxtral-mini-3b-2507", + name: "Voxtral Mini 3B 2507", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.04, + output: 0.04, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "mistral.voxtral-small-24b-2507": { + id: "mistral.voxtral-small-24b-2507", + name: "Voxtral Small 24B 2507", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.35, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "moonshot.kimi-k2-thinking": { + id: "moonshot.kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262143, + maxTokens: 16000, + } satisfies Model<"bedrock-converse-stream">, + "moonshotai.kimi-k2.5": { + id: "moonshotai.kimi-k2.5", + name: "Kimi K2.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262143, + maxTokens: 16000, + } satisfies Model<"bedrock-converse-stream">, + "nvidia.nemotron-nano-12b-v2": { + id: "nvidia.nemotron-nano-12b-v2", + name: "NVIDIA Nemotron Nano 12B v2 VL BF16", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.2, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "nvidia.nemotron-nano-3-30b": { + id: "nvidia.nemotron-nano-3-30b", + name: "NVIDIA Nemotron Nano 3 30B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.24, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "nvidia.nemotron-nano-9b-v2": { + id: "nvidia.nemotron-nano-9b-v2", + name: "NVIDIA Nemotron Nano 9B v2", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.06, + output: 0.23, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"bedrock-converse-stream">, + "nvidia.nemotron-super-3-120b": { + id: "nvidia.nemotron-super-3-120b", + name: "NVIDIA Nemotron 3 Super 120B A12B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.65, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-5.4": { + id: "openai.gpt-5.4", + name: "GPT-5.4", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.75, + output: 16.5, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-5.5": { + id: "openai.gpt-5.5", + name: "GPT-5.5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5.5, + output: 33, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-120b": { + id: "openai.gpt-oss-120b", + name: "gpt-oss-120b", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-120b-1:0": { + id: "openai.gpt-oss-120b-1:0", + name: "gpt-oss-120b", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-20b": { + id: "openai.gpt-oss-20b", + name: "gpt-oss-20b", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.07, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-20b-1:0": { + id: "openai.gpt-oss-20b-1:0", + name: "gpt-oss-20b", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.07, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-safeguard-120b": { + id: "openai.gpt-oss-safeguard-120b", + name: "GPT OSS Safeguard 120B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "openai.gpt-oss-safeguard-20b": { + id: "openai.gpt-oss-safeguard-20b", + name: "GPT OSS Safeguard 20B", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.07, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-235b-a22b-2507-v1:0": { + id: "qwen.qwen3-235b-a22b-2507-v1:0", + name: "Qwen3 235B A22B 2507", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.22, + output: 0.88, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-32b-v1:0": { + id: "qwen.qwen3-32b-v1:0", + name: "Qwen3 32B (dense)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 16384, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-coder-30b-a3b-v1:0": { + id: "qwen.qwen3-coder-30b-a3b-v1:0", + name: "Qwen3 Coder 30B A3B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-coder-480b-a35b-v1:0": { + id: "qwen.qwen3-coder-480b-a35b-v1:0", + name: "Qwen3 Coder 480B A35B Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.22, + output: 1.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-coder-next": { + id: "qwen.qwen3-coder-next", + name: "Qwen3 Coder Next", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.22, + output: 1.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-next-80b-a3b": { + id: "qwen.qwen3-next-80b-a3b", + name: "Qwen/Qwen3-Next-80B-A3B-Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text"], + cost: { + input: 0.14, + output: 1.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"bedrock-converse-stream">, + "qwen.qwen3-vl-235b-a22b": { + id: "qwen.qwen3-vl-235b-a22b", + name: "Qwen/Qwen3-VL-235B-A22B-Instruct", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-fable-5": { + id: "us.anthropic.claude-fable-5", + name: "Claude Fable 5 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-1-20250805-v1:0": { + id: "us.anthropic.claude-opus-4-1-20250805-v1:0", + name: "Claude Opus 4.1 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-5-20251101-v1:0": { + id: "us.anthropic.claude-opus-4-5-20251101-v1:0", + name: "Claude Opus 4.5 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-6-v1": { + id: "us.anthropic.claude-opus-4-6-v1", + name: "Claude Opus 4.6 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-7": { + id: "us.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-8": { + id: "us.anthropic.claude-opus-4-8", + name: "Claude Opus 4.8 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-sonnet-4-6": { + id: "us.anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "us.deepseek.r1-v1:0": { + id: "us.deepseek.r1-v1:0", + name: "DeepSeek-R1 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 1.35, + output: 5.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32768, + } satisfies Model<"bedrock-converse-stream">, + "us.meta.llama4-maverick-17b-instruct-v1:0": { + id: "us.meta.llama4-maverick-17b-instruct-v1:0", + name: "Llama 4 Maverick 17B Instruct (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.24, + output: 0.97, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "us.meta.llama4-scout-17b-instruct-v1:0": { + id: "us.meta.llama4-scout-17b-instruct-v1:0", + name: "Llama 4 Scout 17B Instruct (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.66, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 3500000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "writer.palmyra-x4-v1:0": { + id: "writer.palmyra-x4-v1:0", + name: "Palmyra X4", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 122880, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "writer.palmyra-x5-v1:0": { + id: "writer.palmyra-x5-v1:0", + name: "Palmyra X5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1040000, + maxTokens: 8192, + } satisfies Model<"bedrock-converse-stream">, + "zai.glm-4.7": { + id: "zai.glm-4.7", + name: "GLM-4.7", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "zai.glm-4.7-flash": { + id: "zai.glm-4.7-flash", + name: "GLM-4.7-Flash", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 0.07, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"bedrock-converse-stream">, + "zai.glm-5": { + id: "zai.glm-5", + name: "GLM-5", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 101376, + } satisfies Model<"bedrock-converse-stream">, +} as const; diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 5d327f4b..d839ab6a 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -1,1061 +1,35 @@ -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 { 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"; +import { bedrockConverseStreamApi } from "../api/bedrock-converse-stream.lazy.ts"; +import type { ApiKeyAuth } from "../auth/types.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { AMAZON_BEDROCK_MODELS } from "./amazon-bedrock.models.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 streamBedrock: 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; +/** + * Bedrock auth is ambient: the AWS SDK's default credential chain handles the + * actual signing, so `resolve` only reports whether the provider is + * configured. A stored credential key is surfaced as the bearer token. + */ +const bedrockAuth: ApiKeyAuth = { + name: "AWS credentials", + resolve: async ({ ctx, credential }) => { + if (credential?.key) return { auth: { apiKey: credential.key }, source: "stored credential" }; + if (await ctx.env("AWS_BEARER_TOKEN_BEDROCK")) return { auth: {}, source: "AWS_BEARER_TOKEN_BEDROCK" }; + if (await ctx.env("AWS_PROFILE")) return { auth: {}, source: "AWS_PROFILE" }; + if ((await ctx.env("AWS_ACCESS_KEY_ID")) && (await ctx.env("AWS_SECRET_ACCESS_KEY"))) { + return { auth: {}, source: "AWS access keys" }; } - - // 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); - if (options.headers && Object.keys(options.headers).length > 0) { - addCustomHeadersMiddleware(client, options.headers); - } - 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; + if (await ctx.env("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")) return { auth: {}, source: "ECS task role" }; + if (await ctx.env("AWS_CONTAINER_CREDENTIALS_FULL_URI")) return { auth: {}, source: "ECS task role" }; + if (await ctx.env("AWS_WEB_IDENTITY_TOKEN_FILE")) return { auth: {}, source: "web identity token" }; + return undefined; + }, }; -/** - * 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 streamSimpleBedrock: 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 streamBedrock(model, context, { ...base, reasoning: undefined } satisfies BedrockOptions); - } - - if (isAnthropicClaudeModel(model)) { - if (supportsAdaptiveThinking(model.id, model.name)) { - return streamBedrock(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 streamBedrock(model, context, { - ...base, - maxTokens: adjusted.maxTokens, - reasoning: options.reasoning, - thinkingBudgets: { - ...(options.thinkingBudgets || {}), - [clampReasoning(options.reasoning)!]: adjusted.thinkingBudget, - }, - } satisfies BedrockOptions); - } - - return streamBedrock(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, "-")]; +export function amazonBedrockProvider(): Provider<"bedrock-converse-stream"> { + return createProvider({ + id: "amazon-bedrock", + name: "Amazon Bedrock", + auth: { apiKey: bedrockAuth }, + models: Object.values(AMAZON_BEDROCK_MODELS), + api: bedrockConverseStreamApi(), }); } - -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/ant-ling.models.ts b/packages/ai/src/providers/ant-ling.models.ts new file mode 100644 index 00000000..10c656fe --- /dev/null +++ b/packages/ai/src/providers/ant-ling.models.ts @@ -0,0 +1,62 @@ +// 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 ANT_LING_MODELS = { + "Ling-2.6-1T": { + id: "Ling-2.6-1T", + name: "Ling 2.6 1T", + api: "openai-completions", + provider: "ant-ling", + baseUrl: "https://api.ant-ling.com/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"ant-ling","supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.06, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Ling-2.6-flash": { + id: "Ling-2.6-flash", + name: "Ling 2.6 Flash", + api: "openai-completions", + provider: "ant-ling", + baseUrl: "https://api.ant-ling.com/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"ant-ling","supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.01, + output: 0.02, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Ring-2.6-1T": { + id: "Ring-2.6-1T", + name: "Ring 2.6 1T", + api: "openai-completions", + provider: "ant-ling", + baseUrl: "https://api.ant-ling.com/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"ant-ling","supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, + input: ["text"], + cost: { + input: 0.06, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/ant-ling.ts b/packages/ai/src/providers/ant-ling.ts new file mode 100644 index 00000000..03bb314d --- /dev/null +++ b/packages/ai/src/providers/ant-ling.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { ANT_LING_MODELS } from "./ant-ling.models.ts"; + +export function antLingProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "ant-ling", + name: "Ant Ling", + baseUrl: "https://api.ant-ling.com/v1", + auth: { apiKey: envApiKeyAuth("Ant Ling API key", ["ANT_LING_API_KEY"]) }, + models: Object.values(ANT_LING_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/anthropic.models.ts b/packages/ai/src/providers/anthropic.models.ts new file mode 100644 index 00000000..3db30f74 --- /dev/null +++ b/packages/ai/src/providers/anthropic.models.ts @@ -0,0 +1,441 @@ +// 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 ANTHROPIC_MODELS = { + "claude-3-5-haiku-20241022": { + id: "claude-3-5-haiku-20241022", + name: "Claude Haiku 3.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-5-haiku-latest": { + id: "claude-3-5-haiku-latest", + name: "Claude Haiku 3.5 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-5-sonnet-20240620": { + id: "claude-3-5-sonnet-20240620", + name: "Claude Sonnet 3.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-5-sonnet-20241022": { + id: "claude-3-5-sonnet-20241022", + name: "Claude Sonnet 3.5 v2", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-7-sonnet-20250219": { + id: "claude-3-7-sonnet-20250219", + name: "Claude Sonnet 3.7", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-3-haiku-20240307": { + id: "claude-3-haiku-20240307", + name: "Claude Haiku 3", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.25, + cacheRead: 0.03, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3-opus-20240229": { + id: "claude-3-opus-20240229", + name: "Claude Opus 3", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3-sonnet-20240229": { + id: "claude-3-sonnet-20240229", + name: "Claude Sonnet 3", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-haiku-4-5": { + id: "claude-haiku-4-5", + name: "Claude Haiku 4.5 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-haiku-4-5-20251001": { + id: "claude-haiku-4-5-20251001", + name: "Claude Haiku 4.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-0": { + id: "claude-opus-4-0", + name: "Claude Opus 4 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-1": { + id: "claude-opus-4-1", + name: "Claude Opus 4.1 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-1-20250805": { + id: "claude-opus-4-1-20250805", + name: "Claude Opus 4.1", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-20250514": { + id: "claude-opus-4-20250514", + name: "Claude Opus 4", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-5": { + id: "claude-opus-4-5", + name: "Claude Opus 4.5 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-5-20251101": { + id: "claude-opus-4-5-20251101", + name: "Claude Opus 4.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-6": { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-8": { + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-0": { + id: "claude-sonnet-4-0", + name: "Claude Sonnet 4 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-20250514": { + id: "claude-sonnet-4-20250514", + name: "Claude Sonnet 4", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-5": { + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5 (latest)", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-5-20250929": { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-6": { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 6ffab7cb..6570fc38 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -1,1242 +1,20 @@ -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, - 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 { resolveCloudflareBaseUrl } from "./cloudflare.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> { - // Auto-detect session affinity and cache control support from provider - const isFireworks = model.provider === "fireworks"; - const isCloudflareAiGatewayAnthropic = - model.provider === "cloudflare-ai-gateway" && model.baseUrl.includes("anthropic"); - return { - supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? !isFireworks, - supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? !isFireworks, - sendSessionAffinityHeaders: - model.compat?.sendSessionAffinityHeaders ?? !!(isFireworks || isCloudflareAiGatewayAnthropic), - supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks, - 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 `streamSimpleAnthropic()` 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 `streamSimpleAnthropic()` 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: (Record | undefined)[]): Record { - const merged: Record = {}; - for (const headers of headerSources) { - if (headers) { - Object.assign(merged, headers); - } - } - return merged; -} - -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 streamAnthropic: 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; - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - 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, - options?.env, - ); - 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 streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions> = ( - model: Model<"anthropic-messages">, - 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 streamAnthropic(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 streamAnthropic(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 streamAnthropic(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, - interleavedThinking: boolean, - useFineGrainedToolStreamingBeta: boolean, - optionsHeaders?: Record, - dynamicHeaders?: Record, - sessionId?: string, - env?: ProviderEnv, -): { 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); - } - - if (model.provider === "cloudflare-ai-gateway") { - const client = new Anthropic({ - apiKey: null, - authToken: null, - baseURL: resolveCloudflareBaseUrl(model, env), - dangerouslyAllowBrowser: true, - defaultHeaders: mergeHeaders( - { - accept: "application/json", - "anthropic-dangerous-direct-browser-access": "true", - "cf-aig-authorization": `Bearer ${apiKey}`, - "x-api-key": null, - Authorization: null, - ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}), - }, - model.headers, - optionsHeaders, - ), - }); - - return { client, isOAuthToken: false }; - } - - // Copilot: Bearer auth, selective betas. - if (model.provider === "github-copilot") { - const client = new Anthropic({ - apiKey: null, - authToken: apiKey, - 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 (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 auth - const sessionAffinityHeaders: Record = - sessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { "x-session-affinity": sessionId } : {}; - const client = new Anthropic({ - apiKey, - authToken: null, - baseURL: model.baseUrl, - dangerouslyAllowBrowser: true, - defaultHeaders: mergeHeaders( - { - accept: "application/json", - "anthropic-dangerous-direct-browser-access": "true", - ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}), - }, - sessionAffinityHeaders, - model.headers, - optionsHeaders, - ), - }); - - 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 } : {}), - }; +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { loadAnthropicOAuth } from "../utils/oauth/load.ts"; +import { ANTHROPIC_MODELS } from "./anthropic.models.ts"; + +export function anthropicProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "anthropic", + name: "Anthropic", + baseUrl: "https://api.anthropic.com", + auth: { + // ANTHROPIC_OAUTH_TOKEN takes precedence over ANTHROPIC_API_KEY + apiKey: envApiKeyAuth("Anthropic API key", ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]), + oauth: lazyOAuth({ name: "Anthropic (Claude Pro/Max)", load: loadAnthropicOAuth }), + }, + models: Object.values(ANTHROPIC_MODELS), + api: anthropicMessagesApi(), }); } - -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/providers/azure-openai-responses.models.ts b/packages/ai/src/providers/azure-openai-responses.models.ts new file mode 100644 index 00000000..b35eec5c --- /dev/null +++ b/packages/ai/src/providers/azure-openai-responses.models.ts @@ -0,0 +1,745 @@ +// 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 AZURE_OPENAI_RESPONSES_MODELS = { + "gpt-4": { + id: "gpt-4", + name: "GPT-4", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text"], + cost: { + input: 30, + output: 60, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8192, + maxTokens: 8192, + } satisfies Model<"azure-openai-responses">, + "gpt-4-turbo": { + id: "gpt-4-turbo", + name: "GPT-4 Turbo", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"azure-openai-responses">, + "gpt-4.1": { + id: "gpt-4.1", + name: "GPT-4.1", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"azure-openai-responses">, + "gpt-4.1-mini": { + id: "gpt-4.1-mini", + name: "GPT-4.1 mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 1.6, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"azure-openai-responses">, + "gpt-4.1-nano": { + id: "gpt-4.1-nano", + name: "GPT-4.1 nano", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"azure-openai-responses">, + "gpt-4o": { + id: "gpt-4o", + name: "GPT-4o", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-4o-2024-05-13": { + id: "gpt-4o-2024-05-13", + name: "GPT-4o (2024-05-13)", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 5, + output: 15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"azure-openai-responses">, + "gpt-4o-2024-08-06": { + id: "gpt-4o-2024-08-06", + name: "GPT-4o (2024-08-06)", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-4o-2024-11-20": { + id: "gpt-4o-2024-11-20", + name: "GPT-4o (2024-11-20)", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-5": { + id: "gpt-5", + name: "GPT-5", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5-chat-latest": { + id: "gpt-5-chat-latest", + name: "GPT-5 Chat Latest", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-5-codex": { + id: "gpt-5-codex", + name: "GPT-5-Codex", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5-mini": { + id: "gpt-5-mini", + name: "GPT-5 Mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5-nano": { + id: "gpt-5-nano", + name: "GPT-5 Nano", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.05, + output: 0.4, + cacheRead: 0.005, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5-pro": { + id: "gpt-5-pro", + name: "GPT-5 Pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 15, + output: 120, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.1": { + id: "gpt-5.1", + name: "GPT-5.1", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.1-chat-latest": { + id: "gpt-5.1-chat-latest", + name: "GPT-5.1 Chat", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-5.1-codex": { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.1-codex-max": { + id: "gpt-5.1-codex-max", + name: "GPT-5.1 Codex Max", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.1-codex-mini": { + id: "gpt-5.1-codex-mini", + name: "GPT-5.1 Codex mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.2-chat-latest": { + id: "gpt-5.2-chat-latest", + name: "GPT-5.2 Chat", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.2-pro": { + id: "gpt-5.2-pro", + name: "GPT-5.2 Pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 21, + output: 168, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.3-chat-latest": { + id: "gpt-5.3-chat-latest", + name: "GPT-5.3 Chat (latest)", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: false, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"azure-openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.3-codex-spark": { + id: "gpt-5.3-codex-spark", + name: "GPT-5.3 Codex Spark", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.4-mini": { + id: "gpt-5.4-mini", + name: "GPT-5.4 mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.4-nano": { + id: "gpt-5.4-nano", + name: "GPT-5.4 nano", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.4-pro": { + id: "gpt-5.4-pro", + name: "GPT-5.4 Pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "gpt-5.5-pro": { + id: "gpt-5.5-pro", + name: "GPT-5.5 Pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"azure-openai-responses">, + "o1": { + id: "o1", + name: "o1", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o1-pro": { + id: "o1-pro", + name: "o1-pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 150, + output: 600, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o3": { + id: "o3", + name: "o3", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o3-deep-research": { + id: "o3-deep-research", + name: "o3-deep-research", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 40, + cacheRead: 2.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o3-mini": { + id: "o3-mini", + name: "o3-mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o3-pro": { + id: "o3-pro", + name: "o3-pro", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o4-mini": { + id: "o4-mini", + name: "o4-mini", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, + "o4-mini-deep-research": { + id: "o4-mini-deep-research", + name: "o4-mini-deep-research", + api: "azure-openai-responses", + provider: "azure-openai-responses", + baseUrl: "", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"azure-openai-responses">, +} as const; diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index db1d3fb7..78351dea 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -1,299 +1,14 @@ -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"; +import { azureOpenAIResponsesApi } from "../api/azure-openai-responses.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { AZURE_OPENAI_RESPONSES_MODELS } from "./azure-openai-responses.models.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 streamAzureOpenAIResponses: 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 streamSimpleAzureOpenAIResponses: 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 streamAzureOpenAIResponses(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"); - 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")) { - 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, +export function azureOpenAIResponsesProvider(): Provider<"azure-openai-responses"> { + return createProvider({ + id: "azure-openai-responses", + name: "Azure OpenAI", + auth: { apiKey: envApiKeyAuth("Azure OpenAI API key", ["AZURE_OPENAI_API_KEY"]) }, + models: Object.values(AZURE_OPENAI_RESPONSES_MODELS), + api: azureOpenAIResponsesApi(), }); } - -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/providers/cerebras.models.ts b/packages/ai/src/providers/cerebras.models.ts new file mode 100644 index 00000000..53c4151c --- /dev/null +++ b/packages/ai/src/providers/cerebras.models.ts @@ -0,0 +1,43 @@ +// 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 CEREBRAS_MODELS = { + "gpt-oss-120b": { + id: "gpt-oss-120b", + name: "GPT OSS 120B", + api: "openai-completions", + provider: "cerebras", + baseUrl: "https://api.cerebras.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.35, + output: 0.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 40960, + } satisfies Model<"openai-completions">, + "zai-glm-4.7": { + id: "zai-glm-4.7", + name: "Z.AI GLM-4.7", + api: "openai-completions", + provider: "cerebras", + baseUrl: "https://api.cerebras.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 2.25, + output: 2.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 40960, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/cerebras.ts b/packages/ai/src/providers/cerebras.ts new file mode 100644 index 00000000..9ffc7375 --- /dev/null +++ b/packages/ai/src/providers/cerebras.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { CEREBRAS_MODELS } from "./cerebras.models.ts"; + +export function cerebrasProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "cerebras", + name: "Cerebras", + baseUrl: "https://api.cerebras.ai/v1", + auth: { apiKey: envApiKeyAuth("Cerebras API key", ["CEREBRAS_API_KEY"]) }, + models: Object.values(CEREBRAS_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/cloudflare-ai-gateway.models.ts b/packages/ai/src/providers/cloudflare-ai-gateway.models.ts new file mode 100644 index 00000000..d8a562fe --- /dev/null +++ b/packages/ai/src/providers/cloudflare-ai-gateway.models.ts @@ -0,0 +1,668 @@ +// 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 CLOUDFLARE_AI_GATEWAY_MODELS = { + "claude-3-5-haiku": { + id: "claude-3-5-haiku", + name: "Claude Haiku 3.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-haiku": { + id: "claude-3-haiku", + name: "Claude Haiku 3", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.25, + cacheRead: 0.03, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3-opus": { + id: "claude-3-opus", + name: "Claude Opus 3", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3-sonnet": { + id: "claude-3-sonnet", + name: "Claude Sonnet 3", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3.5-haiku": { + id: "claude-3.5-haiku", + name: "Claude Haiku 3.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3.5-sonnet": { + id: "claude-3.5-sonnet", + name: "Claude Sonnet 3.5 v2", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true,"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-haiku-4-5": { + id: "claude-haiku-4-5", + name: "Claude Haiku 4.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4": { + id: "claude-opus-4", + name: "Claude Opus 4 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-1": { + id: "claude-opus-4-1", + name: "Claude Opus 4.1 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-5": { + id: "claude-opus-4-5", + name: "Claude Opus 4.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-6": { + id: "claude-opus-4-6", + name: "Claude Opus 4.6 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true,"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true,"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-8": { + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true,"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4": { + id: "claude-sonnet-4", + name: "Claude Sonnet 4 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-5": { + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-6": { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"sendSessionAffinityHeaders":true,"forceAdaptiveThinking":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "gpt-4": { + id: "gpt-4", + name: "GPT-4", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text"], + cost: { + input: 30, + output: 60, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8192, + maxTokens: 8192, + } satisfies Model<"openai-responses">, + "gpt-4-turbo": { + id: "gpt-4-turbo", + name: "GPT-4 Turbo", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-responses">, + "gpt-4o": { + id: "gpt-4o", + name: "GPT-4o", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o mini", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5.1": { + id: "gpt-5.1", + name: "GPT-5.1", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex": { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "o1": { + id: "o1", + name: "o1", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3": { + id: "o3", + name: "o3", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-mini": { + id: "o3-mini", + name: "o3-mini", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-pro": { + id: "o3-pro", + name: "o3-pro", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o4-mini": { + id: "o4-mini", + name: "o4-mini", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.28, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "workers-ai/@cf/moonshotai/kimi-k2.5": { + id: "workers-ai/@cf/moonshotai/kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "workers-ai/@cf/moonshotai/kimi-k2.6": { + id: "workers-ai/@cf/moonshotai/kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "workers-ai/@cf/nvidia/nemotron-3-120b-a12b": { + id: "workers-ai/@cf/nvidia/nemotron-3-120b-a12b", + name: "Nemotron 3 Super 120B", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "workers-ai/@cf/zai-org/glm-4.7-flash": { + id: "workers-ai/@cf/zai-org/glm-4.7-flash", + name: "GLM-4.7-Flash", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/cloudflare-ai-gateway.ts b/packages/ai/src/providers/cloudflare-ai-gateway.ts new file mode 100644 index 00000000..9f6ff5f6 --- /dev/null +++ b/packages/ai/src/providers/cloudflare-ai-gateway.ts @@ -0,0 +1,22 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { openAIResponsesApi } from "../api/openai-responses.lazy.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { CLOUDFLARE_AI_GATEWAY_MODELS } from "./cloudflare-ai-gateway.models.ts"; +import { cloudflareAIGatewayAuth } from "./cloudflare-auth.ts"; + +export function cloudflareAIGatewayProvider(): Provider< + "anthropic-messages" | "openai-completions" | "openai-responses" +> { + return createProvider({ + id: "cloudflare-ai-gateway", + name: "Cloudflare AI Gateway", + auth: { apiKey: cloudflareAIGatewayAuth() }, + models: Object.values(CLOUDFLARE_AI_GATEWAY_MODELS), + api: { + "anthropic-messages": anthropicMessagesApi(), + "openai-completions": openAICompletionsApi(), + "openai-responses": openAIResponsesApi(), + }, + }); +} diff --git a/packages/ai/src/providers/cloudflare-auth.ts b/packages/ai/src/providers/cloudflare-auth.ts new file mode 100644 index 00000000..511e8d73 --- /dev/null +++ b/packages/ai/src/providers/cloudflare-auth.ts @@ -0,0 +1,105 @@ +import type { ApiKeyAuth, ApiKeyCredential, AuthContext } from "../auth/types.ts"; +import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv } from "../types.ts"; + +const CLOUDFLARE_API_KEY = "CLOUDFLARE_API_KEY"; +const CLOUDFLARE_ACCOUNT_ID = "CLOUDFLARE_ACCOUNT_ID"; +const CLOUDFLARE_GATEWAY_ID = "CLOUDFLARE_GATEWAY_ID"; + +type CloudflareAuthKind = "workers-ai" | "ai-gateway"; + +async function resolveValue( + name: string, + ctx: AuthContext, + credential: ApiKeyCredential | undefined, +): Promise { + if (credential) { + if (name === CLOUDFLARE_API_KEY) return credential.key; + return credential.env?.[name]; + } + return ctx.env(name); +} + +function resolveCloudflareBaseUrl( + model: Model | ImagesModel, + accountId: string, + gatewayId: string | undefined, +): string { + return model.baseUrl + .replaceAll(`{${CLOUDFLARE_ACCOUNT_ID}}`, accountId) + .replaceAll(`{${CLOUDFLARE_GATEWAY_ID}}`, gatewayId ?? ""); +} + +async function resolveCloudflareEnv( + kind: CloudflareAuthKind, + model: Model | ImagesModel, + ctx: AuthContext, + credential: ApiKeyCredential | undefined, +): Promise<{ apiKey: string; env: ProviderEnv; baseUrl: string; source: string } | undefined> { + const apiKey = await resolveValue(CLOUDFLARE_API_KEY, ctx, credential); + const accountId = await resolveValue(CLOUDFLARE_ACCOUNT_ID, ctx, credential); + const gatewayId = kind === "ai-gateway" ? await resolveValue(CLOUDFLARE_GATEWAY_ID, ctx, credential) : undefined; + + if (!apiKey || !accountId || (kind === "ai-gateway" && !gatewayId)) return undefined; + + return { + apiKey, + env: { + CLOUDFLARE_ACCOUNT_ID: accountId, + ...(gatewayId ? { CLOUDFLARE_GATEWAY_ID: gatewayId } : {}), + }, + baseUrl: resolveCloudflareBaseUrl(model, accountId, gatewayId), + source: credential ? "stored credential" : CLOUDFLARE_API_KEY, + }; +} + +export function cloudflareWorkersAIAuth(): ApiKeyAuth { + return { + name: "Cloudflare API key", + login: async (callbacks) => { + const key = await callbacks.prompt({ type: "secret", message: "Enter Cloudflare API key" }); + const accountId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare account ID" }); + return { type: "api_key", key, env: { CLOUDFLARE_ACCOUNT_ID: accountId } }; + }, + resolve: async ({ model, ctx, credential }) => { + const resolved = await resolveCloudflareEnv("workers-ai", model, ctx, credential); + if (!resolved) return undefined; + return { + auth: { apiKey: resolved.apiKey, baseUrl: resolved.baseUrl }, + env: resolved.env, + source: resolved.source, + }; + }, + }; +} + +export function cloudflareAIGatewayAuth(): ApiKeyAuth { + return { + name: "Cloudflare API key", + login: async (callbacks) => { + const key = await callbacks.prompt({ type: "secret", message: "Enter Cloudflare API key" }); + const accountId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare account ID" }); + const gatewayId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare AI Gateway ID" }); + return { + type: "api_key", + key, + env: { CLOUDFLARE_ACCOUNT_ID: accountId, CLOUDFLARE_GATEWAY_ID: gatewayId }, + }; + }, + resolve: async ({ model, ctx, credential }) => { + const resolved = await resolveCloudflareEnv("ai-gateway", model, ctx, credential); + if (!resolved) return undefined; + return { + auth: { + headers: { + "cf-aig-authorization": `Bearer ${resolved.apiKey}`, + Authorization: null, + "x-api-key": null, + }, + baseUrl: resolved.baseUrl, + }, + env: resolved.env, + source: resolved.source, + }; + }, + }; +} diff --git a/packages/ai/src/providers/cloudflare-workers-ai.models.ts b/packages/ai/src/providers/cloudflare-workers-ai.models.ts new file mode 100644 index 00000000..3adfee60 --- /dev/null +++ b/packages/ai/src/providers/cloudflare-workers-ai.models.ts @@ -0,0 +1,241 @@ +// 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 CLOUDFLARE_WORKERS_AI_MODELS = { + "@cf/google/gemma-4-26b-a4b-it": { + id: "@cf/google/gemma-4-26b-a4b-it", + name: "Gemma 4 26B A4B IT", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "@cf/ibm-granite/granite-4.0-h-micro": { + id: "@cf/ibm-granite/granite-4.0-h-micro", + name: "Granite 4.0 H Micro", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text"], + cost: { + input: 0.017, + output: 0.112, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131000, + maxTokens: 131000, + } satisfies Model<"openai-completions">, + "@cf/meta/llama-3.3-70b-instruct-fp8-fast": { + id: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", + name: "Llama 3.3 70B Instruct fp8 Fast", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text"], + cost: { + input: 0.293, + output: 2.253, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 24000, + maxTokens: 24000, + } satisfies Model<"openai-completions">, + "@cf/meta/llama-4-scout-17b-16e-instruct": { + id: "@cf/meta/llama-4-scout-17b-16e-instruct", + name: "Llama 4 Scout 17B 16E Instruct", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.27, + output: 0.85, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "@cf/mistralai/mistral-small-3.1-24b-instruct": { + id: "@cf/mistralai/mistral-small-3.1-24b-instruct", + name: "Mistral Small 3.1 24B Instruct", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: false, + input: ["text"], + cost: { + input: 0.351, + output: 0.555, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "@cf/moonshotai/kimi-k2.6": { + id: "@cf/moonshotai/kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "@cf/moonshotai/kimi-k2.7-code": { + id: "@cf/moonshotai/kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "@cf/nvidia/nemotron-3-120b-a12b": { + id: "@cf/nvidia/nemotron-3-120b-a12b", + name: "Nemotron 3 Super 120B", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "@cf/openai/gpt-oss-120b": { + id: "@cf/openai/gpt-oss-120b", + name: "GPT OSS 120B", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.35, + output: 0.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "@cf/openai/gpt-oss-20b": { + id: "@cf/openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.2, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "@cf/qwen/qwen3-30b-a3b-fp8": { + id: "@cf/qwen/qwen3-30b-a3b-fp8", + name: "Qwen3 30B A3b fp8", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.0509, + output: 0.335, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "@cf/zai-org/glm-4.7-flash": { + id: "@cf/zai-org/glm-4.7-flash", + name: "GLM-4.7-Flash", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.0605, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "@cf/zai-org/glm-5.2": { + id: "@cf/zai-org/glm-5.2", + name: "Glm 5.2", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/cloudflare-workers-ai.ts b/packages/ai/src/providers/cloudflare-workers-ai.ts new file mode 100644 index 00000000..9e376a5c --- /dev/null +++ b/packages/ai/src/providers/cloudflare-workers-ai.ts @@ -0,0 +1,14 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { cloudflareWorkersAIAuth } from "./cloudflare-auth.ts"; +import { CLOUDFLARE_WORKERS_AI_MODELS } from "./cloudflare-workers-ai.models.ts"; + +export function cloudflareWorkersAIProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "cloudflare-workers-ai", + name: "Cloudflare Workers AI", + auth: { apiKey: cloudflareWorkersAIAuth() }, + models: Object.values(CLOUDFLARE_WORKERS_AI_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/deepseek.models.ts b/packages/ai/src/providers/deepseek.models.ts new file mode 100644 index 00000000..b9bcd95f --- /dev/null +++ b/packages/ai/src/providers/deepseek.models.ts @@ -0,0 +1,45 @@ +// 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 DEEPSEEK_MODELS = { + "deepseek-v4-flash": { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "openai-completions", + provider: "deepseek", + baseUrl: "https://api.deepseek.com", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "deepseek", + baseUrl: "https://api.deepseek.com", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.003625, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/deepseek.ts b/packages/ai/src/providers/deepseek.ts new file mode 100644 index 00000000..580e25c2 --- /dev/null +++ b/packages/ai/src/providers/deepseek.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { DEEPSEEK_MODELS } from "./deepseek.models.ts"; + +export function deepseekProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "deepseek", + name: "DeepSeek", + baseUrl: "https://api.deepseek.com", + auth: { apiKey: envApiKeyAuth("DeepSeek API key", ["DEEPSEEK_API_KEY"]) }, + models: Object.values(DEEPSEEK_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/faux.ts b/packages/ai/src/providers/faux.ts index 7e629847..4a26f1ad 100644 --- a/packages/ai/src/providers/faux.ts +++ b/packages/ai/src/providers/faux.ts @@ -1,4 +1,4 @@ -import { registerApiProvider, unregisterApiProviders } from "../api-registry.ts"; +import { createProvider, type Provider } from "../models.ts"; import type { AssistantMessage, AssistantMessageEventStream, @@ -125,6 +125,18 @@ export interface FauxProviderRegistration { unregister: () => void; } +export interface FauxProviderHandle { + provider: Provider; + api: string; + models: [Model, ...Model[]]; + getModel(): Model; + getModel(modelId: string): Model | undefined; + state: { callCount: number }; + setResponses: (responses: FauxResponseStep[]) => void; + appendResponses: (responses: FauxResponseStep[]) => void; + getPendingResponseCount: () => number; +} + function estimateTokens(text: string): number { return Math.ceil(text.length / 4); } @@ -388,10 +400,9 @@ async function streamWithDeltas( stream.end(message); } -export function registerFauxProvider(options: RegisterFauxProviderOptions = {}): FauxProviderRegistration { +export function createFauxCore(options: RegisterFauxProviderOptions) { const api = options.api ?? randomId(DEFAULT_API); const provider = options.provider ?? DEFAULT_PROVIDER; - const sourceId = randomId("faux-provider"); const minTokenSize = Math.max( 1, Math.min(options.tokenSize?.min ?? DEFAULT_MIN_TOKEN_SIZE, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE), @@ -467,8 +478,6 @@ export function registerFauxProvider(options: RegisterFauxProviderOptions = {}): const streamSimple: StreamFunction = (streamModel, context, streamOptions) => stream(streamModel, context, streamOptions); - registerApiProvider({ api, stream, streamSimple }, sourceId); - function getModel(): Model; function getModel(requestedModelId: string): Model | undefined; function getModel(requestedModelId?: string): Model | undefined { @@ -480,20 +489,50 @@ export function registerFauxProvider(options: RegisterFauxProviderOptions = {}): return { api, + provider, models, + stream, + streamSimple, getModel, state, - setResponses(responses) { + setResponses(responses: FauxResponseStep[]) { pendingResponses = [...responses]; }, - appendResponses(responses) { + appendResponses(responses: FauxResponseStep[]) { pendingResponses.push(...responses); }, getPendingResponseCount() { return pendingResponses.length; }, - unregister() { - unregisterApiProviders(sourceId); - }, + }; +} + +/** + * Faux provider for tests built on explicit `Models` collections: + * + * ```ts + * const faux = fauxProvider(); + * const models = createModels(); + * models.setProvider(faux.provider); + * faux.setResponses([fauxAssistantMessage("hi")]); + * ``` + */ +export function fauxProvider(options: RegisterFauxProviderOptions = {}): FauxProviderHandle { + const core = createFauxCore(options); + const provider = createProvider({ + id: core.provider, + auth: { apiKey: { name: "Faux", resolve: async () => ({ auth: {} }) } }, + models: core.models, + api: { stream: core.stream, streamSimple: core.streamSimple }, + }); + return { + provider, + api: core.api, + models: core.models, + getModel: core.getModel, + state: core.state, + setResponses: core.setResponses, + appendResponses: core.appendResponses, + getPendingResponseCount: core.getPendingResponseCount, }; } diff --git a/packages/ai/src/providers/fireworks.models.ts b/packages/ai/src/providers/fireworks.models.ts new file mode 100644 index 00000000..cb93d846 --- /dev/null +++ b/packages/ai/src/providers/fireworks.models.ts @@ -0,0 +1,278 @@ +// 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 FIREWORKS_MODELS = { + "accounts/fireworks/models/deepseek-v4-flash": { + id: "accounts/fireworks/models/deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/deepseek-v4-pro": { + id: "accounts/fireworks/models/deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1.74, + output: 3.48, + cacheRead: 0.145, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/glm-5p1": { + id: "accounts/fireworks/models/glm-5p1", + name: "GLM 5.1", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 202800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/glm-5p2": { + id: "accounts/fireworks/models/glm-5p2", + name: "GLM 5.2", + api: "openai-completions", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false}, + reasoning: true, + thinkingLevelMap: {"off":"none","minimal":null,"low":"high","medium":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "accounts/fireworks/models/gpt-oss-120b": { + id: "accounts/fireworks/models/gpt-oss-120b", + name: "GPT OSS 120B", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/gpt-oss-20b": { + id: "accounts/fireworks/models/gpt-oss-20b", + name: "GPT OSS 20B", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.07, + output: 0.3, + cacheRead: 0.035, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/kimi-k2p6": { + id: "accounts/fireworks/models/kimi-k2p6", + name: "Kimi K2.6", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/kimi-k2p7-code": { + id: "accounts/fireworks/models/kimi-k2p7-code", + name: "Kimi K2.7 Code", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/minimax-m2p7": { + id: "accounts/fireworks/models/minimax-m2p7", + name: "MiniMax-M2.7", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 196608, + maxTokens: 196608, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/minimax-m3": { + id: "accounts/fireworks/models/minimax-m3", + name: "MiniMax-M3", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 512000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/qwen3p7-plus": { + id: "accounts/fireworks/models/qwen3p7-plus", + name: "Qwen 3.7 Plus", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 1.6, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/routers/glm-5p1-fast": { + id: "accounts/fireworks/routers/glm-5p1-fast", + name: "GLM 5.1 Fast", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 2.8, + output: 8.8, + cacheRead: 0.52, + cacheWrite: 0, + }, + contextWindow: 202800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/routers/kimi-k2p6-fast": { + id: "accounts/fireworks/routers/kimi-k2p6-fast", + name: "Kimi K2.6 Fast", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.3, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/routers/kimi-k2p6-turbo": { + id: "accounts/fireworks/routers/kimi-k2p6-turbo", + name: "Kimi K2.6 Turbo", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.3, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/routers/kimi-k2p7-code-fast": { + id: "accounts/fireworks/routers/kimi-k2p7-code-fast", + name: "Kimi K2.7 Code Fast", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.9, + output: 8, + cacheRead: 0.38, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/fireworks.ts b/packages/ai/src/providers/fireworks.ts new file mode 100644 index 00000000..518fb259 --- /dev/null +++ b/packages/ai/src/providers/fireworks.ts @@ -0,0 +1,19 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { FIREWORKS_MODELS } from "./fireworks.models.ts"; + +export function fireworksProvider(): Provider<"anthropic-messages" | "openai-completions"> { + return createProvider({ + id: "fireworks", + name: "Fireworks", + baseUrl: "https://api.fireworks.ai/inference", + auth: { apiKey: envApiKeyAuth("Fireworks API key", ["FIREWORKS_API_KEY"]) }, + models: Object.values(FIREWORKS_MODELS), + api: { + "anthropic-messages": anthropicMessagesApi(), + "openai-completions": openAICompletionsApi(), + }, + }); +} diff --git a/packages/ai/src/providers/github-copilot.models.ts b/packages/ai/src/providers/github-copilot.models.ts new file mode 100644 index 00000000..cf866ec2 --- /dev/null +++ b/packages/ai/src/providers/github-copilot.models.ts @@ -0,0 +1,428 @@ +// 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 GITHUB_COPILOT_MODELS = { + "claude-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "claude-haiku-4.5": { + id: "claude-haiku-4.5", + name: "Claude Haiku 4.5 (latest)", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsEagerToolInputStreaming":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4.5": { + id: "claude-opus-4.5", + name: "Claude Opus 4.5 (latest)", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4.6": { + id: "claude-opus-4.6", + name: "Claude Opus 4.6", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4.7": { + id: "claude-opus-4.7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4.8": { + id: "claude-opus-4.8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4": { + id: "claude-sonnet-4", + name: "Claude Sonnet 4 (latest)", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsEagerToolInputStreaming":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 216000, + maxTokens: 16000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4.5": { + id: "claude-sonnet-4.5", + name: "Claude Sonnet 4.5 (latest)", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsEagerToolInputStreaming":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4.6": { + id: "claude-sonnet-4.6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"minimal":"low","xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "gemini-2.5-pro": { + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "gemini-3-flash-preview": { + id: "gemini-3-flash-preview", + name: "Gemini 3 Flash Preview", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "gemini-3.1-pro-preview": { + id: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "gpt-4.1": { + id: "gpt-4.1", + name: "GPT-4.1", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "gpt-5-mini": { + id: "gpt-5-mini", + name: "GPT-5 Mini", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low"}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 264000, + maxTokens: 64000, + } satisfies Model<"openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-mini": { + id: "gpt-5.4-mini", + name: "GPT-5.4 mini", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-nano": { + id: "gpt-5.4-nano", + name: "GPT-5.4 nano", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-responses", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, +} as const; diff --git a/packages/ai/src/providers/github-copilot.ts b/packages/ai/src/providers/github-copilot.ts new file mode 100644 index 00000000..c935ad5d --- /dev/null +++ b/packages/ai/src/providers/github-copilot.ts @@ -0,0 +1,25 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { openAIResponsesApi } from "../api/openai-responses.lazy.ts"; +import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { loadGitHubCopilotOAuth } from "../utils/oauth/load.ts"; +import { GITHUB_COPILOT_MODELS } from "./github-copilot.models.ts"; + +export function githubCopilotProvider(): Provider<"anthropic-messages" | "openai-completions" | "openai-responses"> { + return createProvider({ + id: "github-copilot", + name: "GitHub Copilot", + baseUrl: "https://api.individual.githubcopilot.com", + auth: { + apiKey: envApiKeyAuth("GitHub Copilot token", ["COPILOT_GITHUB_TOKEN"]), + oauth: lazyOAuth({ name: "GitHub Copilot", load: loadGitHubCopilotOAuth }), + }, + models: Object.values(GITHUB_COPILOT_MODELS), + api: { + "anthropic-messages": anthropicMessagesApi(), + "openai-completions": openAICompletionsApi(), + "openai-responses": openAIResponsesApi(), + }, + }); +} diff --git a/packages/ai/src/providers/google-vertex.models.ts b/packages/ai/src/providers/google-vertex.models.ts new file mode 100644 index 00000000..8dfa2414 --- /dev/null +++ b/packages/ai/src/providers/google-vertex.models.ts @@ -0,0 +1,184 @@ +// 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 GOOGLE_VERTEX_MODELS = { + "gemini-2.5-flash": { + id: "gemini-2.5-flash", + name: "Gemini 2.5 Flash", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-2.5-flash-lite": { + id: "gemini-2.5-flash-lite", + name: "Gemini 2.5 Flash-Lite", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-2.5-pro": { + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-3-flash-preview": { + id: "gemini-3-flash-preview", + name: "Gemini 3 Flash Preview", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-3.1-flash-lite": { + id: "gemini-3.1-flash-lite", + name: "Gemini 3.1 Flash Lite", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-3.1-pro-preview": { + id: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-3.1-pro-preview-customtools": { + id: "gemini-3.1-pro-preview-customtools", + name: "Gemini 3.1 Pro Preview Custom Tools", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-flash-latest": { + id: "gemini-flash-latest", + name: "Gemini Flash Latest", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-flash-lite-latest": { + id: "gemini-flash-lite-latest", + name: "Gemini Flash-Lite Latest", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, +} as const; diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index aa971959..af84fc70 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -1,582 +1,38 @@ -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, - SimpleStreamOptions, - StreamFunction, - StreamOptions, - TextContent, - ThinkingBudgets, - ThinkingContent, - ToolCall, -} from "../types.ts"; -import { AssistantMessageEventStream } from "../utils/event-stream.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"; +import { googleVertexApi } from "../api/google-vertex.lazy.ts"; +import type { ApiKeyAuth } from "../auth/types.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { GOOGLE_VERTEX_MODELS } from "./google-vertex.models.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 VERTEX_ADC_PATH = "~/.config/gcloud/application_default_credentials.json"; -const API_VERSION = "v1"; -const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials"; +/** + * Vertex accepts an explicit API key or Application Default Credentials + * (`gcloud auth application-default login`). ADC additionally requires + * project and location env vars, which the implementation reads itself. + */ +const vertexAuth: ApiKeyAuth = { + name: "Google Cloud credentials", + resolve: async ({ ctx, credential }) => { + const key = credential?.key ?? (await ctx.env("GOOGLE_CLOUD_API_KEY")); + if (key) return { auth: { apiKey: key }, source: credential?.key ? "stored credential" : "GOOGLE_CLOUD_API_KEY" }; -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 streamGoogleVertex: 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(); + const adcPath = await ctx.env("GOOGLE_APPLICATION_CREDENTIALS"); + const hasCredentials = await ctx.fileExists(adcPath ?? VERTEX_ADC_PATH); + const hasProject = Boolean((await ctx.env("GOOGLE_CLOUD_PROJECT")) ?? (await ctx.env("GCLOUD_PROJECT"))); + const hasLocation = Boolean(await ctx.env("GOOGLE_CLOUD_LOCATION")); + if (hasCredentials && hasProject && hasLocation) { + return { auth: {}, source: "gcloud application default credentials" }; } - })(); - - return stream; + return undefined; + }, }; -export const streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStreamOptions> = ( - model: Model<"google-vertex">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const base = buildBaseOptions(model, options, undefined); - if (!options?.reasoning) { - return streamGoogleVertex(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 streamGoogleVertex(model, context, { - ...base, - thinking: { - enabled: true, - level: getGemini3ThinkingLevel(effort, geminiModel), - }, - } satisfies GoogleVertexOptions); - } - - return streamGoogleVertex(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?: Record, - env?: ProviderEnv, -): GoogleGenAI { - const googleAuthOptions = buildGoogleAuthOptions(env); - return new GoogleGenAI({ - vertexai: true, - project, - location, - apiVersion: API_VERSION, - ...(googleAuthOptions ? { googleAuthOptions } : {}), - httpOptions: buildHttpOptions(model, optionsHeaders), +export function googleVertexProvider(): Provider<"google-vertex"> { + return createProvider({ + id: "google-vertex", + name: "Google Vertex AI", + auth: { apiKey: vertexAuth }, + models: Object.values(GOOGLE_VERTEX_MODELS), + api: googleVertexApi(), }); } - -function createClientWithApiKey( - model: Model<"google-vertex">, - apiKey: string, - optionsHeaders?: Record, -): GoogleGenAI { - return new GoogleGenAI({ - vertexai: true, - apiKey, - apiVersion: API_VERSION, - httpOptions: buildHttpOptions(model, optionsHeaders), - }); -} - -function buildHttpOptions( - model: Model<"google-vertex">, - optionsHeaders?: Record, -): HttpOptions | undefined { - const httpOptions: HttpOptions = {}; - const baseUrl = resolveCustomBaseUrl(model.baseUrl); - if (baseUrl) { - httpOptions.baseUrl = baseUrl; - httpOptions.baseUrlResourceScope = ResourceScope.COLLECTION; - if (baseUrlIncludesApiVersion(baseUrl)) { - httpOptions.apiVersion = ""; - } - } - - if (model.headers || optionsHeaders) { - httpOptions.headers = { ...model.headers, ...optionsHeaders }; - } - - 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/providers/google.models.ts b/packages/ai/src/providers/google.models.ts new file mode 100644 index 00000000..334e3b43 --- /dev/null +++ b/packages/ai/src/providers/google.models.ts @@ -0,0 +1,290 @@ +// 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 GOOGLE_MODELS = { + "gemini-2.0-flash": { + id: "gemini-2.0-flash", + name: "Gemini 2.0 Flash", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 8192, + } satisfies Model<"google-generative-ai">, + "gemini-2.0-flash-lite": { + id: "gemini-2.0-flash-lite", + name: "Gemini 2.0 Flash-Lite", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 8192, + } satisfies Model<"google-generative-ai">, + "gemini-2.5-flash": { + id: "gemini-2.5-flash", + name: "Gemini 2.5 Flash", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-2.5-flash-lite": { + id: "gemini-2.5-flash-lite", + name: "Gemini 2.5 Flash-Lite", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-2.5-pro": { + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3-flash-preview": { + id: "gemini-3-flash-preview", + name: "Gemini 3 Flash Preview", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3-pro-preview": { + id: "gemini-3-pro-preview", + name: "Gemini 3 Pro Preview", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.1-flash-lite": { + id: "gemini-3.1-flash-lite", + name: "Gemini 3.1 Flash Lite", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.1-flash-lite-preview": { + id: "gemini-3.1-flash-lite-preview", + name: "Gemini 3.1 Flash Lite Preview", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.1-pro-preview": { + id: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.1-pro-preview-customtools": { + id: "gemini-3.1-pro-preview-customtools", + name: "Gemini 3.1 Pro Preview Custom Tools", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-flash-latest": { + id: "gemini-flash-latest", + name: "Gemini Flash Latest", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-flash-lite-latest": { + id: "gemini-flash-lite-latest", + name: "Gemini Flash-Lite Latest", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemma-4-26b-a4b-it": { + id: "gemma-4-26b-a4b-it", + name: "Gemma 4 26B A4B IT", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"google-generative-ai">, + "gemma-4-31b-it": { + id: "gemma-4-31b-it", + name: "Gemma 4 31B IT", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"google-generative-ai">, +} as const; diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index a270792a..0bd45237 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -1,504 +1,15 @@ -import { - type GenerateContentConfig, - type GenerateContentParameters, - GoogleGenAI, - type ThinkingConfig, -} from "@google/genai"; -import { calculateCost, clampThinkingLevel } from "../models.ts"; -import type { - Api, - AssistantMessage, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, - TextContent, - ThinkingBudgets, - ThinkingContent, - ThinkingLevel, - ToolCall, -} from "../types.ts"; -import { AssistantMessageEventStream } from "../utils/event-stream.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"; +import { googleGenerativeAIApi } from "../api/google-generative-ai.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { GOOGLE_MODELS } from "./google.models.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 streamGoogle: 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 streamSimpleGoogle: 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 streamGoogle(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 streamGoogle(model, context, { - ...base, - thinking: { - enabled: true, - level: getThinkingLevel(effort, googleModel), - }, - } satisfies GoogleOptions); - } - - return streamGoogle(model, context, { - ...base, - thinking: { - enabled: true, - budgetTokens: getGoogleBudget(googleModel, effort, options.thinkingBudgets), - }, - } satisfies GoogleOptions); -}; - -function createClient( - model: Model<"google-generative-ai">, - apiKey?: string, - optionsHeaders?: Record, -): 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 - } - if (model.headers || optionsHeaders) { - httpOptions.headers = { ...model.headers, ...optionsHeaders }; - } - - return new GoogleGenAI({ - apiKey, - httpOptions: Object.keys(httpOptions).length > 0 ? httpOptions : undefined, +export function googleProvider(): Provider<"google-generative-ai"> { + return createProvider({ + id: "google", + name: "Google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + auth: { apiKey: envApiKeyAuth("Gemini API key", ["GEMINI_API_KEY"]) }, + models: Object.values(GOOGLE_MODELS), + api: googleGenerativeAIApi(), }); } - -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/groq.models.ts b/packages/ai/src/providers/groq.models.ts new file mode 100644 index 00000000..857048c7 --- /dev/null +++ b/packages/ai/src/providers/groq.models.ts @@ -0,0 +1,127 @@ +// 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 GROQ_MODELS = { + "llama-3.1-8b-instant": { + id: "llama-3.1-8b-instant", + name: "Llama 3.1 8B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.05, + output: 0.08, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "llama-3.3-70b-versatile": { + id: "llama-3.3-70b-versatile", + name: "Llama 3.3 70B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.59, + output: 0.79, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "meta-llama/llama-4-scout-17b-16e-instruct": { + id: "meta-llama/llama-4-scout-17b-16e-instruct", + name: "Llama 4 Scout 17B 16E", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.11, + output: 0.34, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT OSS 120B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0.0375, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-safeguard-20b": { + id: "openai/gpt-oss-safeguard-20b", + name: "Safety GPT OSS 20B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0.037, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3-32b": { + id: "qwen/qwen3-32b", + name: "Qwen3-32B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"default"}, + input: ["text"], + cost: { + input: 0.29, + output: 0.59, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 40960, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/groq.ts b/packages/ai/src/providers/groq.ts new file mode 100644 index 00000000..5892e048 --- /dev/null +++ b/packages/ai/src/providers/groq.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { GROQ_MODELS } from "./groq.models.ts"; + +export function groqProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "groq", + name: "Groq", + baseUrl: "https://api.groq.com/openai/v1", + auth: { apiKey: envApiKeyAuth("Groq API key", ["GROQ_API_KEY"]) }, + models: Object.values(GROQ_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/huggingface.models.ts b/packages/ai/src/providers/huggingface.models.ts new file mode 100644 index 00000000..ffdcdaf9 --- /dev/null +++ b/packages/ai/src/providers/huggingface.models.ts @@ -0,0 +1,799 @@ +// 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 HUGGINGFACE_MODELS = { + "MiniMaxAI/MiniMax-M2": { + id: "MiniMaxAI/MiniMax-M2", + name: "MiniMax-M2", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "MiniMaxAI/MiniMax-M2.1": { + id: "MiniMaxAI/MiniMax-M2.1", + name: "MiniMax-M2.1", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "MiniMaxAI/MiniMax-M2.5": { + id: "MiniMaxAI/MiniMax-M2.5", + name: "MiniMax-M2.5", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "MiniMaxAI/MiniMax-M2.7": { + id: "MiniMaxAI/MiniMax-M2.7", + name: "MiniMax-M2.7", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "MiniMaxAI/MiniMax-M3": { + id: "MiniMaxAI/MiniMax-M3", + name: "MiniMax-M3", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 524288, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-235B-A22B": { + id: "Qwen/Qwen3-235B-A22B", + name: "Qwen3 235B-A22B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.2, + output: 0.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 40960, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-235B-A22B-Thinking-2507": { + id: "Qwen/Qwen3-235B-A22B-Thinking-2507", + name: "Qwen3-235B-A22B-Thinking-2507", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-32B": { + id: "Qwen/Qwen3-32B", + name: "Qwen3 32B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.29, + output: 0.59, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Coder-30B-A3B-Instruct": { + id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", + name: "Qwen3-Coder 30B-A3B Instruct", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.07, + output: 0.26, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Coder-480B-A35B-Instruct": { + id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", + name: "Qwen3-Coder-480B-A35B-Instruct", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 66536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Coder-Next": { + id: "Qwen/Qwen3-Coder-Next", + name: "Qwen3-Coder-Next", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.2, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Next-80B-A3B-Instruct": { + id: "Qwen/Qwen3-Next-80B-A3B-Instruct", + name: "Qwen3-Next-80B-A3B-Instruct", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.25, + output: 1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 66536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-Next-80B-A3B-Thinking": { + id: "Qwen/Qwen3-Next-80B-A3B-Thinking", + name: "Qwen3-Next-80B-A3B-Thinking", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-122B-A10B": { + id: "Qwen/Qwen3.5-122B-A10B", + name: "Qwen3.5 122B-A10B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 3.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-27B": { + id: "Qwen/Qwen3.5-27B", + name: "Qwen3.5 27B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-35B-A3B": { + id: "Qwen/Qwen3.5-35B-A3B", + name: "Qwen3.5 35B-A3B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-397B-A17B": { + id: "Qwen/Qwen3.5-397B-A17B", + name: "Qwen3.5-397B-A17B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-9B": { + id: "Qwen/Qwen3.5-9B", + name: "Qwen3.5 9B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.6-35B-A3B": { + id: "Qwen/Qwen3.6-35B-A3B", + name: "Qwen3.6 35B-A3B", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.95, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "XiaomiMiMo/MiMo-V2-Flash": { + id: "XiaomiMiMo/MiMo-V2-Flash", + name: "MiMo-V2-Flash", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-R1": { + id: "deepseek-ai/DeepSeek-R1", + name: "DeepSeek-R1", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.7, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 64000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-R1-0528": { + id: "deepseek-ai/DeepSeek-R1-0528", + name: "DeepSeek-R1-0528", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 3, + output: 5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 163840, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-V3.2": { + id: "deepseek-ai/DeepSeek-V3.2", + name: "DeepSeek-V3.2", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.28, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-V4-Flash": { + id: "deepseek-ai/DeepSeek-V4-Flash", + name: "DeepSeek V4 Flash", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-V4-Pro": { + id: "deepseek-ai/DeepSeek-V4-Pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.003625, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 393216, + } satisfies Model<"openai-completions">, + "google/gemma-4-26B-A4B-it": { + id: "google/gemma-4-26B-A4B-it", + name: "Gemma 4 26B A4B IT", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.13, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "google/gemma-4-31B-it": { + id: "google/gemma-4-31B-it", + name: "Gemma 4 31B IT", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "meta-llama/Llama-3.3-70B-Instruct": { + id: "meta-llama/Llama-3.3-70B-Instruct", + name: "Llama-3.3-70B-Instruct", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.59, + output: 0.79, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2-Instruct": { + id: "moonshotai/Kimi-K2-Instruct", + name: "Kimi-K2-Instruct", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2-Instruct-0905": { + id: "moonshotai/Kimi-K2-Instruct-0905", + name: "Kimi-K2-Instruct-0905", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2-Thinking": { + id: "moonshotai/Kimi-K2-Thinking", + name: "Kimi-K2-Thinking", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.5": { + id: "moonshotai/Kimi-K2.5", + name: "Kimi-K2.5", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.6": { + id: "moonshotai/Kimi-K2.6", + name: "Kimi-K2.6", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.7-Code": { + id: "moonshotai/Kimi-K2.7-Code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "stepfun-ai/Step-3.5-Flash": { + id: "stepfun-ai/Step-3.5-Flash", + name: "Step 3.5 Flash", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.5": { + id: "zai-org/GLM-4.5", + name: "GLM-4.5", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.5-Air": { + id: "zai-org/GLM-4.5-Air", + name: "GLM-4.5-Air", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.13, + output: 0.85, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.5V": { + id: "zai-org/GLM-4.5V", + name: "GLM-4.5V", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 1.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 65536, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.6": { + id: "zai-org/GLM-4.6", + name: "GLM-4.6", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.55, + output: 2.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.7": { + id: "zai-org/GLM-4.7", + name: "GLM-4.7", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-4.7-Flash": { + id: "zai-org/GLM-4.7-Flash", + name: "GLM-4.7-Flash", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5": { + id: "zai-org/GLM-5", + name: "GLM-5", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5.1": { + id: "zai-org/GLM-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5.2": { + id: "zai-org/GLM-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/huggingface.ts b/packages/ai/src/providers/huggingface.ts new file mode 100644 index 00000000..e8fb628e --- /dev/null +++ b/packages/ai/src/providers/huggingface.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { HUGGINGFACE_MODELS } from "./huggingface.models.ts"; + +export function huggingfaceProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "huggingface", + name: "Hugging Face", + baseUrl: "https://router.huggingface.co/v1", + auth: { apiKey: envApiKeyAuth("Hugging Face token", ["HF_TOKEN"]) }, + models: Object.values(HUGGINGFACE_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/images/register-builtins.ts b/packages/ai/src/providers/images/register-builtins.ts index e3decbb9..a5c901fa 100644 --- a/packages/ai/src/providers/images/register-builtins.ts +++ b/packages/ai/src/providers/images/register-builtins.ts @@ -1,9 +1,9 @@ +import type { generateImages as generateImagesOpenRouterFunction } from "../../api/openrouter-images.ts"; import { registerImagesApiProvider } from "../../images-api-registry.ts"; import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.ts"; -import type { generateImagesOpenRouter as generateImagesOpenRouterFunction } from "./openrouter.ts"; interface OpenRouterImagesProviderModule { - generateImagesOpenRouter: typeof generateImagesOpenRouterFunction; + generateImages: typeof generateImagesOpenRouterFunction; } let openRouterImagesProviderModulePromise: Promise | undefined; @@ -21,7 +21,7 @@ function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, erro } function loadOpenRouterImagesProviderModule(): Promise { - openRouterImagesProviderModulePromise ||= import("./openrouter.ts").then( + openRouterImagesProviderModulePromise ||= import("../../api/openrouter-images.ts").then( (module) => module as OpenRouterImagesProviderModule, ); return openRouterImagesProviderModulePromise; @@ -34,7 +34,7 @@ export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", Image ) => { try { const module = await loadOpenRouterImagesProviderModule(); - return await module.generateImagesOpenRouter(model, context, options); + return await module.generateImages(model, context, options); } catch (error) { return createLazyLoadErrorImages(model, error); } diff --git a/packages/ai/src/providers/kimi-coding.models.ts b/packages/ai/src/providers/kimi-coding.models.ts new file mode 100644 index 00000000..a3b1f266 --- /dev/null +++ b/packages/ai/src/providers/kimi-coding.models.ts @@ -0,0 +1,61 @@ +// 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 KIMI_CODING_MODELS = { + "k2p7": { + id: "k2p7", + name: "Kimi K2.7 Code", + api: "anthropic-messages", + provider: "kimi-coding", + baseUrl: "https://api.kimi.com/coding", + headers: {"User-Agent":"KimiCLI/1.5"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "kimi-for-coding": { + id: "kimi-for-coding", + name: "Kimi For Coding", + api: "anthropic-messages", + provider: "kimi-coding", + baseUrl: "https://api.kimi.com/coding", + headers: {"User-Agent":"KimiCLI/1.5"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "kimi-k2-thinking": { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "anthropic-messages", + provider: "kimi-coding", + baseUrl: "https://api.kimi.com/coding", + headers: {"User-Agent":"KimiCLI/1.5"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/kimi-coding.ts b/packages/ai/src/providers/kimi-coding.ts new file mode 100644 index 00000000..865ae28c --- /dev/null +++ b/packages/ai/src/providers/kimi-coding.ts @@ -0,0 +1,15 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { KIMI_CODING_MODELS } from "./kimi-coding.models.ts"; + +export function kimiCodingProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "kimi-coding", + name: "Kimi For Coding", + baseUrl: "https://api.kimi.com/coding", + auth: { apiKey: envApiKeyAuth("Kimi API key", ["KIMI_API_KEY"]) }, + models: Object.values(KIMI_CODING_MODELS), + api: anthropicMessagesApi(), + }); +} diff --git a/packages/ai/src/providers/minimax-cn.models.ts b/packages/ai/src/providers/minimax-cn.models.ts new file mode 100644 index 00000000..d1f90c21 --- /dev/null +++ b/packages/ai/src/providers/minimax-cn.models.ts @@ -0,0 +1,58 @@ +// 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 MINIMAX_CN_MODELS = { + "MiniMax-M2.7": { + id: "MiniMax-M2.7", + name: "MiniMax-M2.7", + api: "anthropic-messages", + provider: "minimax-cn", + baseUrl: "https://api.minimaxi.com/anthropic", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "MiniMax-M2.7-highspeed": { + id: "MiniMax-M2.7-highspeed", + name: "MiniMax-M2.7-highspeed", + api: "anthropic-messages", + provider: "minimax-cn", + baseUrl: "https://api.minimaxi.com/anthropic", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "MiniMax-M3": { + id: "MiniMax-M3", + name: "MiniMax-M3", + api: "anthropic-messages", + provider: "minimax-cn", + baseUrl: "https://api.minimaxi.com/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/minimax-cn.ts b/packages/ai/src/providers/minimax-cn.ts new file mode 100644 index 00000000..5cbe5acc --- /dev/null +++ b/packages/ai/src/providers/minimax-cn.ts @@ -0,0 +1,15 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { MINIMAX_CN_MODELS } from "./minimax-cn.models.ts"; + +export function minimaxCnProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "minimax-cn", + name: "MiniMax CN", + baseUrl: "https://api.minimaxi.com/anthropic", + auth: { apiKey: envApiKeyAuth("MiniMax CN API key", ["MINIMAX_CN_API_KEY"]) }, + models: Object.values(MINIMAX_CN_MODELS), + api: anthropicMessagesApi(), + }); +} diff --git a/packages/ai/src/providers/minimax.models.ts b/packages/ai/src/providers/minimax.models.ts new file mode 100644 index 00000000..0ff346c7 --- /dev/null +++ b/packages/ai/src/providers/minimax.models.ts @@ -0,0 +1,58 @@ +// 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 MINIMAX_MODELS = { + "MiniMax-M2.7": { + id: "MiniMax-M2.7", + name: "MiniMax-M2.7", + api: "anthropic-messages", + provider: "minimax", + baseUrl: "https://api.minimax.io/anthropic", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "MiniMax-M2.7-highspeed": { + id: "MiniMax-M2.7-highspeed", + name: "MiniMax-M2.7-highspeed", + api: "anthropic-messages", + provider: "minimax", + baseUrl: "https://api.minimax.io/anthropic", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "MiniMax-M3": { + id: "MiniMax-M3", + name: "MiniMax-M3", + api: "anthropic-messages", + provider: "minimax", + baseUrl: "https://api.minimax.io/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/minimax.ts b/packages/ai/src/providers/minimax.ts new file mode 100644 index 00000000..6a956bd6 --- /dev/null +++ b/packages/ai/src/providers/minimax.ts @@ -0,0 +1,15 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { MINIMAX_MODELS } from "./minimax.models.ts"; + +export function minimaxProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "minimax", + name: "MiniMax", + baseUrl: "https://api.minimax.io/anthropic", + auth: { apiKey: envApiKeyAuth("MiniMax API key", ["MINIMAX_API_KEY"]) }, + models: Object.values(MINIMAX_MODELS), + api: anthropicMessagesApi(), + }); +} diff --git a/packages/ai/src/providers/mistral.models.ts b/packages/ai/src/providers/mistral.models.ts new file mode 100644 index 00000000..7060772b --- /dev/null +++ b/packages/ai/src/providers/mistral.models.ts @@ -0,0 +1,517 @@ +// 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 MISTRAL_MODELS = { + "codestral-latest": { + id: "codestral-latest", + name: "Codestral (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 0.9, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"mistral-conversations">, + "devstral-2512": { + id: "devstral-2512", + name: "Devstral 2", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "devstral-latest": { + id: "devstral-latest", + name: "Devstral 2", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "devstral-medium-2507": { + id: "devstral-medium-2507", + name: "Devstral Medium", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "devstral-medium-latest": { + id: "devstral-medium-latest", + name: "Devstral 2 (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "devstral-small-2505": { + id: "devstral-small-2505", + name: "Devstral Small 2505", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "devstral-small-2507": { + id: "devstral-small-2507", + name: "Devstral Small", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "labs-devstral-small-2512": { + id: "labs-devstral-small-2512", + name: "Devstral Small 2", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"mistral-conversations">, + "magistral-medium-latest": { + id: "magistral-medium-latest", + name: "Magistral Medium (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text"], + cost: { + input: 2, + output: 5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"mistral-conversations">, + "magistral-small": { + id: "magistral-small", + name: "Magistral Small", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "ministral-3b-latest": { + id: "ministral-3b-latest", + name: "Ministral 3B (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.04, + output: 0.04, + cacheRead: 0.004, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "ministral-8b-latest": { + id: "ministral-8b-latest", + name: "Ministral 8B (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.1, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "mistral-large-2411": { + id: "mistral-large-2411", + name: "Mistral Large 2.1", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 6, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"mistral-conversations">, + "mistral-large-2512": { + id: "mistral-large-2512", + name: "Mistral Large 3", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-large-latest": { + id: "mistral-large-latest", + name: "Mistral Large (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-medium-2505": { + id: "mistral-medium-2505", + name: "Mistral Medium 3", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"mistral-conversations">, + "mistral-medium-2508": { + id: "mistral-medium-2508", + name: "Mistral Medium 3.1", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-medium-2604": { + id: "mistral-medium-2604", + name: "Mistral Medium 3.5", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-medium-3.5": { + id: "mistral-medium-3.5", + name: "Mistral Medium 3.5", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-medium-latest": { + id: "mistral-medium-latest", + name: "Mistral Medium (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, + "mistral-nemo": { + id: "mistral-nemo", + name: "Mistral Nemo", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "mistral-small-2506": { + id: "mistral-small-2506", + name: "Mistral Small 3.2", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"mistral-conversations">, + "mistral-small-2603": { + id: "mistral-small-2603", + name: "Mistral Small 4", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"mistral-conversations">, + "mistral-small-latest": { + id: "mistral-small-latest", + name: "Mistral Small (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"mistral-conversations">, + "open-mistral-7b": { + id: "open-mistral-7b", + name: "Mistral 7B", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.25, + output: 0.25, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 8000, + maxTokens: 8000, + } satisfies Model<"mistral-conversations">, + "open-mistral-nemo": { + id: "open-mistral-nemo", + name: "Open Mistral Nemo", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "open-mixtral-8x22b": { + id: "open-mixtral-8x22b", + name: "Mixtral 8x22B", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 6, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 64000, + maxTokens: 64000, + } satisfies Model<"mistral-conversations">, + "open-mixtral-8x7b": { + id: "open-mixtral-8x7b", + name: "Mixtral 8x7B", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text"], + cost: { + input: 0.7, + output: 0.7, + cacheRead: 0.07, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 32000, + } satisfies Model<"mistral-conversations">, + "pixtral-12b": { + id: "pixtral-12b", + name: "Pixtral 12B", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, + "pixtral-large-latest": { + id: "pixtral-large-latest", + name: "Pixtral Large (latest)", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 6, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"mistral-conversations">, +} as const; diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts index 1bc7d4ce..9b84a71f 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -1,633 +1,15 @@ -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"; +import { mistralConversationsApi } from "../api/mistral-conversations.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { MISTRAL_MODELS } from "./mistral.models.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 streamMistral: 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 streamSimpleMistral: 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 streamMistral(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 (options?.sessionId && !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 (context.systemPrompt) { - payload.messages.unshift({ - role: "system", - content: sanitizeSurrogates(context.systemPrompt), - }); - } - - return payload; -} - -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) { - output.usage.input = chunk.usage.promptTokens || 0; - output.usage.output = chunk.usage.completionTokens || 0; - output.usage.cacheRead = 0; - output.usage.cacheWrite = 0; - output.usage.totalTokens = chunk.usage.totalTokens || output.usage.input + output.usage.output; - 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"; - } +export function mistralProvider(): Provider<"mistral-conversations"> { + return createProvider({ + id: "mistral", + name: "Mistral", + baseUrl: "https://api.mistral.ai", + auth: { apiKey: envApiKeyAuth("Mistral API key", ["MISTRAL_API_KEY"]) }, + models: Object.values(MISTRAL_MODELS), + api: mistralConversationsApi(), + }); } diff --git a/packages/ai/src/providers/moonshotai-cn.models.ts b/packages/ai/src/providers/moonshotai-cn.models.ts new file mode 100644 index 00000000..899f9b11 --- /dev/null +++ b/packages/ai/src/providers/moonshotai-cn.models.ts @@ -0,0 +1,171 @@ +// 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 MOONSHOTAI_CN_MODELS = { + "kimi-k2-0711-preview": { + id: "kimi-k2-0711-preview", + name: "Kimi K2 0711", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "kimi-k2-0905-preview": { + id: "kimi-k2-0905-preview", + name: "Kimi K2 0905", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-thinking": { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-thinking-turbo": { + id: "kimi-k2-thinking-turbo", + name: "Kimi K2 Thinking Turbo", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.15, + output: 8, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-turbo-preview": { + id: "kimi-k2-turbo-preview", + name: "Kimi K2 Turbo", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 2.4, + output: 10, + cacheRead: 0.6, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.5": { + id: "kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.6": { + id: "kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.7-code-highspeed": { + id: "kimi-k2.7-code-highspeed", + name: "Kimi K2.7 Code HighSpeed", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.9, + output: 8, + cacheRead: 0.38, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/moonshotai-cn.ts b/packages/ai/src/providers/moonshotai-cn.ts new file mode 100644 index 00000000..b813734b --- /dev/null +++ b/packages/ai/src/providers/moonshotai-cn.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { MOONSHOTAI_CN_MODELS } from "./moonshotai-cn.models.ts"; + +export function moonshotaiCnProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "moonshotai-cn", + name: "Moonshot AI CN", + baseUrl: "https://api.moonshot.cn/v1", + auth: { apiKey: envApiKeyAuth("Moonshot AI API key", ["MOONSHOT_API_KEY"]) }, + models: Object.values(MOONSHOTAI_CN_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/moonshotai.models.ts b/packages/ai/src/providers/moonshotai.models.ts new file mode 100644 index 00000000..2ec685e6 --- /dev/null +++ b/packages/ai/src/providers/moonshotai.models.ts @@ -0,0 +1,171 @@ +// 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 MOONSHOTAI_MODELS = { + "kimi-k2-0711-preview": { + id: "kimi-k2-0711-preview", + name: "Kimi K2 0711", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "kimi-k2-0905-preview": { + id: "kimi-k2-0905-preview", + name: "Kimi K2 0905", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-thinking": { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-thinking-turbo": { + id: "kimi-k2-thinking-turbo", + name: "Kimi K2 Thinking Turbo", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.15, + output: 8, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-turbo-preview": { + id: "kimi-k2-turbo-preview", + name: "Kimi K2 Turbo", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: false, + input: ["text"], + cost: { + input: 2.4, + output: 10, + cacheRead: 0.6, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.5": { + id: "kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.6": { + id: "kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2.7-code-highspeed": { + id: "kimi-k2.7-code-highspeed", + name: "Kimi K2.7 Code HighSpeed", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.9, + output: 8, + cacheRead: 0.38, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/moonshotai.ts b/packages/ai/src/providers/moonshotai.ts new file mode 100644 index 00000000..dc15c570 --- /dev/null +++ b/packages/ai/src/providers/moonshotai.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { MOONSHOTAI_MODELS } from "./moonshotai.models.ts"; + +export function moonshotaiProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "moonshotai", + name: "Moonshot AI", + baseUrl: "https://api.moonshot.ai/v1", + auth: { apiKey: envApiKeyAuth("Moonshot AI API key", ["MOONSHOT_API_KEY"]) }, + models: Object.values(MOONSHOTAI_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/nvidia.models.ts b/packages/ai/src/providers/nvidia.models.ts new file mode 100644 index 00000000..d0a0c713 --- /dev/null +++ b/packages/ai/src/providers/nvidia.models.ts @@ -0,0 +1,368 @@ +// 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 NVIDIA_MODELS = { + "meta/llama-3.1-70b-instruct": { + id: "meta/llama-3.1-70b-instruct", + name: "Llama 3.1 70b Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "meta/llama-3.1-8b-instruct": { + id: "meta/llama-3.1-8b-instruct", + name: "Llama 3.1 8B Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 16000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "meta/llama-3.2-11b-vision-instruct": { + id: "meta/llama-3.2-11b-vision-instruct", + name: "Llama 3.2 11b Vision Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "meta/llama-3.2-90b-vision-instruct": { + id: "meta/llama-3.2-90b-vision-instruct", + name: "Llama-3.2-90B-Vision-Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "meta/llama-3.3-70b-instruct": { + id: "meta/llama-3.3-70b-instruct", + name: "Llama 3.3 70b Instruct", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-large-3-675b-instruct-2512": { + id: "mistralai/mistral-large-3-675b-instruct-2512", + name: "Mistral Large 3 675B Instruct 2512", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "mistralai/mistral-small-4-119b-2603": { + id: "mistralai/mistral-small-4-119b-2603", + name: "mistral-small-4-119b-2603", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2.6": { + id: "moonshotai/kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-nano-30b-a3b": { + id: "nvidia/nemotron-3-nano-30b-a3b", + name: "nemotron-3-nano-30b-a3b", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": { + id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", + name: "Nemotron 3 Nano Omni", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-super-120b-a12b": { + id: "nvidia/nemotron-3-super-120b-a12b", + name: "Nemotron 3 Super", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.2, + output: 0.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-ultra-550b-a55b": { + id: "nvidia/nemotron-3-ultra-550b-a55b", + name: "Nemotron 3 Ultra 550B A55B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "nvidia/nvidia-nemotron-nano-9b-v2": { + id: "nvidia/nvidia-nemotron-nano-9b-v2", + name: "nvidia-nemotron-nano-9b-v2", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT-OSS-120B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-122b-a10b": { + id: "qwen/qwen3.5-122b-a10b", + name: "Qwen3.5 122B-A10B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "stepfun-ai/step-3.5-flash": { + id: "stepfun-ai/step-3.5-flash", + name: "Step 3.5 Flash", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "stepfun-ai/step-3.7-flash": { + id: "stepfun-ai/step-3.7-flash", + name: "Step 3.7 Flash", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "z-ai/glm-5.1": { + id: "z-ai/glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/nvidia.ts b/packages/ai/src/providers/nvidia.ts new file mode 100644 index 00000000..dc539f60 --- /dev/null +++ b/packages/ai/src/providers/nvidia.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { NVIDIA_MODELS } from "./nvidia.models.ts"; + +export function nvidiaProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "nvidia", + name: "NVIDIA", + baseUrl: "https://integrate.api.nvidia.com/v1", + auth: { apiKey: envApiKeyAuth("NVIDIA API key", ["NVIDIA_API_KEY"]) }, + models: Object.values(NVIDIA_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/openai-codex.models.ts b/packages/ai/src/providers/openai-codex.models.ts new file mode 100644 index 00000000..c849c24c --- /dev/null +++ b/packages/ai/src/providers/openai-codex.models.ts @@ -0,0 +1,79 @@ +// 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 OPENAI_CODEX_MODELS = { + "gpt-5.3-codex-spark": { + id: "gpt-5.3-codex-spark", + name: "GPT-5.3 Codex Spark", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, + "gpt-5.4-mini": { + id: "gpt-5.4-mini", + name: "GPT-5.4 mini", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, +} as const; diff --git a/packages/ai/src/providers/openai-codex.ts b/packages/ai/src/providers/openai-codex.ts new file mode 100644 index 00000000..6ccdb6ef --- /dev/null +++ b/packages/ai/src/providers/openai-codex.ts @@ -0,0 +1,18 @@ +import { openAICodexResponsesApi } from "../api/openai-codex-responses.lazy.ts"; +import { lazyOAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { loadOpenAICodexOAuth } from "../utils/oauth/load.ts"; +import { OPENAI_CODEX_MODELS } from "./openai-codex.models.ts"; + +export function openaiCodexProvider(): Provider<"openai-codex-responses"> { + return createProvider({ + id: "openai-codex", + name: "OpenAI Codex", + baseUrl: "https://chatgpt.com/backend-api", + auth: { + oauth: lazyOAuth({ name: "OpenAI (ChatGPT Plus/Pro)", load: loadOpenAICodexOAuth }), + }, + models: Object.values(OPENAI_CODEX_MODELS), + api: openAICodexResponsesApi(), + }); +} diff --git a/packages/ai/src/providers/openai.models.ts b/packages/ai/src/providers/openai.models.ts new file mode 100644 index 00000000..fcf9a76c --- /dev/null +++ b/packages/ai/src/providers/openai.models.ts @@ -0,0 +1,745 @@ +// 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 OPENAI_MODELS = { + "gpt-4": { + id: "gpt-4", + name: "GPT-4", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text"], + cost: { + input: 30, + output: 60, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8192, + maxTokens: 8192, + } satisfies Model<"openai-responses">, + "gpt-4-turbo": { + id: "gpt-4-turbo", + name: "GPT-4 Turbo", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-responses">, + "gpt-4.1": { + id: "gpt-4.1", + name: "GPT-4.1", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"openai-responses">, + "gpt-4.1-mini": { + id: "gpt-4.1-mini", + name: "GPT-4.1 mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 1.6, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"openai-responses">, + "gpt-4.1-nano": { + id: "gpt-4.1-nano", + name: "GPT-4.1 nano", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"openai-responses">, + "gpt-4o": { + id: "gpt-4o", + name: "GPT-4o", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-4o-2024-05-13": { + id: "gpt-4o-2024-05-13", + name: "GPT-4o (2024-05-13)", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 5, + output: 15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-responses">, + "gpt-4o-2024-08-06": { + id: "gpt-4o-2024-08-06", + name: "GPT-4o (2024-08-06)", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-4o-2024-11-20": { + id: "gpt-4o-2024-11-20", + name: "GPT-4o (2024-11-20)", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5": { + id: "gpt-5", + name: "GPT-5", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-chat-latest": { + id: "gpt-5-chat-latest", + name: "GPT-5 Chat Latest", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5-codex": { + id: "gpt-5-codex", + name: "GPT-5-Codex", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-mini": { + id: "gpt-5-mini", + name: "GPT-5 Mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-nano": { + id: "gpt-5-nano", + name: "GPT-5 Nano", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.05, + output: 0.4, + cacheRead: 0.005, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-pro": { + id: "gpt-5-pro", + name: "GPT-5 Pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 15, + output: 120, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1": { + id: "gpt-5.1", + name: "GPT-5.1", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none"}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-chat-latest": { + id: "gpt-5.1-chat-latest", + name: "GPT-5.1 Chat", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex": { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex-max": { + id: "gpt-5.1-codex-max", + name: "GPT-5.1 Codex Max", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex-mini": { + id: "gpt-5.1-codex-mini", + name: "GPT-5.1 Codex mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-chat-latest": { + id: "gpt-5.2-chat-latest", + name: "GPT-5.2 Chat", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-pro": { + id: "gpt-5.2-pro", + name: "GPT-5.2 Pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 21, + output: 168, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-chat-latest": { + id: "gpt-5.3-chat-latest", + name: "GPT-5.3 Chat (latest)", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: false, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex-spark": { + id: "gpt-5.3-codex-spark", + name: "GPT-5.3 Codex Spark", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32000, + } satisfies Model<"openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-mini": { + id: "gpt-5.4-mini", + name: "GPT-5.4 mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-nano": { + id: "gpt-5.4-nano", + name: "GPT-5.4 nano", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-pro": { + id: "gpt-5.4-pro", + name: "GPT-5.4 Pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":"none","xhigh":"xhigh","minimal":null}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5-pro": { + id: "gpt-5.5-pro", + name: "GPT-5.5 Pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "o1": { + id: "o1", + name: "o1", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o1-pro": { + id: "o1-pro", + name: "o1-pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 150, + output: 600, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3": { + id: "o3", + name: "o3", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-deep-research": { + id: "o3-deep-research", + name: "o3-deep-research", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 40, + cacheRead: 2.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-mini": { + id: "o3-mini", + name: "o3-mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-pro": { + id: "o3-pro", + name: "o3-pro", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o4-mini": { + id: "o4-mini", + name: "o4-mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o4-mini-deep-research": { + id: "o4-mini-deep-research", + name: "o4-mini-deep-research", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, +} as const; diff --git a/packages/ai/src/providers/openai.ts b/packages/ai/src/providers/openai.ts new file mode 100644 index 00000000..43f6671f --- /dev/null +++ b/packages/ai/src/providers/openai.ts @@ -0,0 +1,15 @@ +import { openAIResponsesApi } from "../api/openai-responses.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { OPENAI_MODELS } from "./openai.models.ts"; + +export function openaiProvider(): Provider<"openai-responses"> { + return createProvider({ + id: "openai", + name: "OpenAI", + baseUrl: "https://api.openai.com/v1", + auth: { apiKey: envApiKeyAuth("OpenAI API key", ["OPENAI_API_KEY"]) }, + models: Object.values(OPENAI_MODELS), + api: openAIResponsesApi(), + }); +} diff --git a/packages/ai/src/providers/opencode-go.models.ts b/packages/ai/src/providers/opencode-go.models.ts new file mode 100644 index 00000000..6cf15918 --- /dev/null +++ b/packages/ai/src/providers/opencode-go.models.ts @@ -0,0 +1,242 @@ +// 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 OPENCODE_GO_MODELS = { + "deepseek-v4-flash": { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 1.74, + output: 3.48, + cacheRead: 0.0145, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "glm-5.1": { + id: "glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "kimi-k2.6": { + id: "kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "mimo-v2.5": { + id: "mimo-v2.5", + name: "MiMo V2.5", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro": { + id: "mimo-v2.5-pro", + name: "MiMo V2.5 Pro", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.74, + output: 3.48, + cacheRead: 0.0145, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "minimax-m2.7": { + id: "minimax-m2.7", + name: "MiniMax M2.7", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "minimax-m3": { + id: "minimax-m3", + name: "MiniMax M3 (3x usage)", + api: "anthropic-messages", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "qwen3.6-plus": { + id: "qwen3.6-plus", + name: "Qwen3.6 Plus", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"thinkingFormat":"qwen","maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0.625, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen3.7-max": { + id: "qwen3.7-max", + name: "Qwen3.7 Max", + api: "anthropic-messages", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go", + reasoning: true, + input: ["text"], + cost: { + input: 2.5, + output: 7.5, + cacheRead: 0.5, + cacheWrite: 3.125, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "qwen3.7-plus": { + id: "qwen3.7-plus", + name: "Qwen3.7 Plus", + api: "anthropic-messages", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 1.6, + cacheRead: 0.04, + cacheWrite: 0.5, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/opencode-go.ts b/packages/ai/src/providers/opencode-go.ts new file mode 100644 index 00000000..608f579b --- /dev/null +++ b/packages/ai/src/providers/opencode-go.ts @@ -0,0 +1,18 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { OPENCODE_GO_MODELS } from "./opencode-go.models.ts"; + +export function opencodeGoProvider(): Provider<"anthropic-messages" | "openai-completions"> { + return createProvider({ + id: "opencode-go", + name: "OpenCode Zen Go", + auth: { apiKey: envApiKeyAuth("OpenCode API key", ["OPENCODE_API_KEY"]) }, + models: Object.values(OPENCODE_GO_MODELS), + api: { + "anthropic-messages": anthropicMessagesApi(), + "openai-completions": openAICompletionsApi(), + }, + }); +} diff --git a/packages/ai/src/providers/opencode.models.ts b/packages/ai/src/providers/opencode.models.ts new file mode 100644 index 00000000..3da37f41 --- /dev/null +++ b/packages/ai/src/providers/opencode.models.ts @@ -0,0 +1,817 @@ +// 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 OPENCODE_MODELS = { + "big-pickle": { + id: "big-pickle", + name: "Big Pickle", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "claude-haiku-4-5": { + id: "claude-haiku-4-5", + name: "Claude Haiku 4.5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-1": { + id: "claude-opus-4-1", + name: "Claude Opus 4.1", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-5": { + id: "claude-opus-4-5", + name: "Claude Opus 4.5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-6": { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-8": { + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4": { + id: "claude-sonnet-4", + name: "Claude Sonnet 4", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-5": { + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-6": { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "deepseek-v4-flash": { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "deepseek-v4-flash-free": { + id: "deepseek-v4-flash-free", + name: "DeepSeek V4 Flash Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 1.74, + output: 3.84, + cacheRead: 0.145, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "gemini-3-flash": { + id: "gemini-3-flash", + name: "Gemini 3 Flash", + api: "google-generative-ai", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.1-pro": { + id: "gemini-3.1-pro", + name: "Gemini 3.1 Pro Preview", + api: "google-generative-ai", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "google-generative-ai", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "glm-5": { + id: "glm-5", + name: "GLM-5", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5.1": { + id: "glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "gpt-5": { + id: "gpt-5", + name: "GPT-5", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-codex": { + id: "gpt-5-codex", + name: "GPT-5 Codex", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-nano": { + id: "gpt-5-nano", + name: "GPT-5 Nano", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.05, + output: 0.4, + cacheRead: 0.005, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1": { + id: "gpt-5.1", + name: "GPT-5.1", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex": { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex-max": { + id: "gpt-5.1-codex-max", + name: "GPT-5.1 Codex Max", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex-mini": { + id: "gpt-5.1-codex-mini", + name: "GPT-5.1 Codex Mini", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-mini": { + id: "gpt-5.4-mini", + name: "GPT-5.4 Mini", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-nano": { + id: "gpt-5.4-nano", + name: "GPT-5.4 Nano", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4-pro": { + id: "gpt-5.4-pro", + name: "GPT-5.4 Pro", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 30, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5-pro": { + id: "gpt-5.5-pro", + name: "GPT-5.5 Pro", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","minimal":null,"low":null}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 30, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "grok-build-0.1": { + id: "grok-build-0.1", + name: "Grok Build 0.1", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "kimi-k2.5": { + id: "kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "kimi-k2.6": { + id: "kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "mimo-v2.5-free": { + id: "mimo-v2.5-free", + name: "MiMo V2.5 Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "minimax-m2.5": { + id: "minimax-m2.5", + name: "MiniMax M2.5", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "minimax-m2.7": { + id: "minimax-m2.7", + name: "MiniMax M2.7", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "nemotron-3-ultra-free": { + id: "nemotron-3-ultra-free", + name: "Nemotron 3 Ultra Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "north-mini-code-free": { + id: "north-mini-code-free", + name: "North Mini Code Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "qwen3.5-plus": { + id: "qwen3.5-plus", + name: "Qwen3.5 Plus", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.2, + cacheRead: 0.02, + cacheWrite: 0.25, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "qwen3.6-plus": { + id: "qwen3.6-plus", + name: "Qwen3.6 Plus", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0.625, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/opencode.ts b/packages/ai/src/providers/opencode.ts new file mode 100644 index 00000000..7d6d2cf7 --- /dev/null +++ b/packages/ai/src/providers/opencode.ts @@ -0,0 +1,24 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { googleGenerativeAIApi } from "../api/google-generative-ai.lazy.ts"; +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { openAIResponsesApi } from "../api/openai-responses.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { OPENCODE_MODELS } from "./opencode.models.ts"; + +export function opencodeProvider(): Provider< + "anthropic-messages" | "google-generative-ai" | "openai-completions" | "openai-responses" +> { + return createProvider({ + id: "opencode", + name: "OpenCode Zen", + auth: { apiKey: envApiKeyAuth("OpenCode API key", ["OPENCODE_API_KEY"]) }, + models: Object.values(OPENCODE_MODELS), + api: { + "anthropic-messages": anthropicMessagesApi(), + "google-generative-ai": googleGenerativeAIApi(), + "openai-completions": openAICompletionsApi(), + "openai-responses": openAIResponsesApi(), + }, + }); +} diff --git a/packages/ai/src/providers/openrouter-images.ts b/packages/ai/src/providers/openrouter-images.ts new file mode 100644 index 00000000..7047cf0e --- /dev/null +++ b/packages/ai/src/providers/openrouter-images.ts @@ -0,0 +1,14 @@ +import { openrouterImagesApi } from "../api/openrouter-images.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { IMAGE_MODELS } from "../image-models.generated.ts"; +import { createImagesProvider, type ImagesProvider } from "../images-models.ts"; + +export function openrouterImagesProvider(): ImagesProvider { + return createImagesProvider({ + id: "openrouter", + name: "OpenRouter", + auth: { apiKey: envApiKeyAuth("OpenRouter API key", ["OPENROUTER_API_KEY"]) }, + models: Object.values(IMAGE_MODELS.openrouter), + api: openrouterImagesApi(), + }); +} diff --git a/packages/ai/src/providers/openrouter.models.ts b/packages/ai/src/providers/openrouter.models.ts new file mode 100644 index 00000000..0dad51de --- /dev/null +++ b/packages/ai/src/providers/openrouter.models.ts @@ -0,0 +1,4637 @@ +// 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 OPENROUTER_MODELS = { + "ai21/jamba-large-1.7": { + id: "ai21/jamba-large-1.7", + name: "AI21: Jamba Large 1.7", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "amazon/nova-2-lite-v1": { + id: "amazon/nova-2-lite-v1", + name: "Amazon: Nova 2 Lite", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "amazon/nova-lite-v1": { + id: "amazon/nova-lite-v1", + name: "Amazon: Nova Lite 1.0", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.06, + output: 0.24, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 300000, + maxTokens: 5120, + } satisfies Model<"openai-completions">, + "amazon/nova-micro-v1": { + id: "amazon/nova-micro-v1", + name: "Amazon: Nova Micro 1.0", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.035, + output: 0.14, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 5120, + } satisfies Model<"openai-completions">, + "amazon/nova-premier-v1": { + id: "amazon/nova-premier-v1", + name: "Amazon: Nova Premier 1.0", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 12.5, + cacheRead: 0.625, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "amazon/nova-pro-v1": { + id: "amazon/nova-pro-v1", + name: "Amazon: Nova Pro 1.0", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 3.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 300000, + maxTokens: 5120, + } satisfies Model<"openai-completions">, + "anthropic/claude-3-haiku": { + id: "anthropic/claude-3-haiku", + name: "Anthropic: Claude 3 Haiku", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.25, + cacheRead: 0.03, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "anthropic/claude-fable-5": { + id: "anthropic/claude-fable-5", + name: "Anthropic: Claude Fable 5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-haiku-4.5": { + id: "anthropic/claude-haiku-4.5", + name: "Anthropic: Claude Haiku 4.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4": { + id: "anthropic/claude-opus-4", + name: "Anthropic: Claude Opus 4", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.1": { + id: "anthropic/claude-opus-4.1", + name: "Anthropic: Claude Opus 4.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.5": { + id: "anthropic/claude-opus-4.5", + name: "Anthropic: Claude Opus 4.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.6": { + id: "anthropic/claude-opus-4.6", + name: "Anthropic: Claude Opus 4.6", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.6-fast": { + id: "anthropic/claude-opus-4.6-fast", + name: "Anthropic: Claude Opus 4.6 (Fast)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 30, + output: 150, + cacheRead: 3, + cacheWrite: 37.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.7": { + id: "anthropic/claude-opus-4.7", + name: "Anthropic: Claude Opus 4.7", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.7-fast": { + id: "anthropic/claude-opus-4.7-fast", + name: "Anthropic: Claude Opus 4.7 (Fast)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 150, + cacheRead: 3, + cacheWrite: 37.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.8": { + id: "anthropic/claude-opus-4.8", + name: "Anthropic: Claude Opus 4.8", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.8-fast": { + id: "anthropic/claude-opus-4.8-fast", + name: "Anthropic: Claude Opus 4.8 (Fast)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "anthropic/claude-sonnet-4": { + id: "anthropic/claude-sonnet-4", + name: "Anthropic: Claude Sonnet 4", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "anthropic/claude-sonnet-4.5": { + id: "anthropic/claude-sonnet-4.5", + name: "Anthropic: Claude Sonnet 4.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "anthropic/claude-sonnet-4.6": { + id: "anthropic/claude-sonnet-4.6", + name: "Anthropic: Claude Sonnet 4.6", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter","cacheControlFormat":"anthropic"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "arcee-ai/trinity-large-thinking": { + id: "arcee-ai/trinity-large-thinking", + name: "Arcee AI: Trinity Large Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.25, + output: 0.8, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 80000, + } satisfies Model<"openai-completions">, + "arcee-ai/trinity-mini": { + id: "arcee-ai/trinity-mini", + name: "Arcee AI: Trinity Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.045, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "arcee-ai/virtuoso-large": { + id: "arcee-ai/virtuoso-large", + name: "Arcee AI: Virtuoso Large", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.75, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "auto": { + id: "auto", + name: "Auto", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, + "bytedance-seed/seed-1.6": { + id: "bytedance-seed/seed-1.6", + name: "ByteDance Seed: Seed 1.6", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "bytedance-seed/seed-1.6-flash": { + id: "bytedance-seed/seed-1.6-flash", + name: "ByteDance Seed: Seed 1.6 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "bytedance-seed/seed-2.0-lite": { + id: "bytedance-seed/seed-2.0-lite", + name: "ByteDance Seed: Seed-2.0-Lite", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "bytedance-seed/seed-2.0-mini": { + id: "bytedance-seed/seed-2.0-mini", + name: "ByteDance Seed: Seed-2.0-Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "cohere/command-r-08-2024": { + id: "cohere/command-r-08-2024", + name: "Cohere: Command R (08-2024)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"openai-completions">, + "cohere/command-r-plus-08-2024": { + id: "cohere/command-r-plus-08-2024", + name: "Cohere: Command R+ (08-2024)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"openai-completions">, + "cohere/north-mini-code:free": { + id: "cohere/north-mini-code:free", + name: "Cohere: North Mini Code (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-chat": { + id: "deepseek/deepseek-chat", + name: "DeepSeek: DeepSeek V3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.2002, + output: 0.8001, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16000, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-chat-v3-0324": { + id: "deepseek/deepseek-chat-v3-0324", + name: "DeepSeek: DeepSeek V3 0324", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.2, + output: 0.77, + cacheRead: 0.135, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-chat-v3.1": { + id: "deepseek/deepseek-chat-v3.1", + name: "DeepSeek: DeepSeek V3.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.21, + output: 0.79, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-r1": { + id: "deepseek/deepseek-r1", + name: "DeepSeek: R1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.7, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 16000, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-r1-0528": { + id: "deepseek/deepseek-r1-0528", + name: "DeepSeek: R1 0528", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 2.15, + cacheRead: 0.35, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v3.1-terminus": { + id: "deepseek/deepseek-v3.1-terminus", + name: "DeepSeek: DeepSeek V3.1 Terminus", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.27, + output: 0.95, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v3.2": { + id: "deepseek/deepseek-v3.2", + name: "DeepSeek: DeepSeek V3.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.2288, + output: 0.3432, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v3.2-exp": { + id: "deepseek/deepseek-v3.2-exp", + name: "DeepSeek: DeepSeek V3.2 Exp", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.27, + output: 0.41, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v4-flash": { + id: "deepseek/deepseek-v4-flash", + name: "DeepSeek: DeepSeek V4 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter","requiresReasoningContentOnAssistantMessages":true}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, + input: ["text"], + cost: { + input: 0.09, + output: 0.18, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "deepseek/deepseek-v4-pro": { + id: "deepseek/deepseek-v4-pro", + name: "DeepSeek: DeepSeek V4 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter","requiresReasoningContentOnAssistantMessages":true}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, + input: ["text"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.003625, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-flash": { + id: "google/gemini-2.5-flash", + name: "Google: Gemini 2.5 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0.03, + cacheWrite: 0.083333, + }, + contextWindow: 1048576, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-flash-lite": { + id: "google/gemini-2.5-flash-lite", + name: "Google: Gemini 2.5 Flash Lite", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0.083333, + }, + contextWindow: 1048576, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-flash-lite-preview-09-2025": { + id: "google/gemini-2.5-flash-lite-preview-09-2025", + name: "Google: Gemini 2.5 Flash Lite Preview 09-2025", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0.083333, + }, + contextWindow: 1048576, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-pro": { + id: "google/gemini-2.5-pro", + name: "Google: Gemini 2.5 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0.375, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-pro-preview": { + id: "google/gemini-2.5-pro-preview", + name: "Google: Gemini 2.5 Pro Preview 06-05", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0.375, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-2.5-pro-preview-05-06": { + id: "google/gemini-2.5-pro-preview-05-06", + name: "Google: Gemini 2.5 Pro Preview 05-06", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0.375, + }, + contextWindow: 1048576, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-3-flash-preview": { + id: "google/gemini-3-flash-preview", + name: "Google: Gemini 3 Flash Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0.083333, + }, + contextWindow: 1048576, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-3-pro-image": { + id: "google/gemini-3-pro-image", + name: "Google: Nano Banana Pro (Gemini 3 Pro Image)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0.375, + }, + contextWindow: 65536, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "google/gemini-3.1-flash-lite": { + id: "google/gemini-3.1-flash-lite", + name: "Google: Gemini 3.1 Flash Lite", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.025, + cacheWrite: 0.083333, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-3.1-flash-lite-preview": { + id: "google/gemini-3.1-flash-lite-preview", + name: "Google: Gemini 3.1 Flash Lite Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.025, + cacheWrite: 0.083333, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-3.1-pro-preview": { + id: "google/gemini-3.1-pro-preview", + name: "Google: Gemini 3.1 Pro Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0.375, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-3.1-pro-preview-customtools": { + id: "google/gemini-3.1-pro-preview-customtools", + name: "Google: Gemini 3.1 Pro Preview Custom Tools", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0.375, + }, + contextWindow: 1048756, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemini-3.5-flash": { + id: "google/gemini-3.5-flash", + name: "Google: Gemini 3.5 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0.083333, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "google/gemma-3-12b-it": { + id: "google/gemma-3-12b-it", + name: "Google: Gemma 3 12B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.05, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "google/gemma-3-27b-it": { + id: "google/gemma-3-27b-it", + name: "Google: Gemma 3 27B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.08, + output: 0.16, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "google/gemma-4-26b-a4b-it": { + id: "google/gemma-4-26b-a4b-it", + name: "Google: Gemma 4 26B A4B ", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.06, + output: 0.33, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "google/gemma-4-26b-a4b-it:free": { + id: "google/gemma-4-26b-a4b-it:free", + name: "Google: Gemma 4 26B A4B (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "google/gemma-4-31b-it": { + id: "google/gemma-4-31b-it", + name: "Google: Gemma 4 31B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.12, + output: 0.35, + cacheRead: 0.09, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "google/gemma-4-31b-it:free": { + id: "google/gemma-4-31b-it:free", + name: "Google: Gemma 4 31B (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "ibm-granite/granite-4.1-8b": { + id: "ibm-granite/granite-4.1-8b", + name: "IBM: Granite 4.1 8B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.05, + output: 0.1, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "inception/mercury-2": { + id: "inception/mercury-2", + name: "Inception: Mercury 2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text"], + cost: { + input: 0.25, + output: 0.75, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 50000, + } satisfies Model<"openai-completions">, + "inclusionai/ling-2.6-1t": { + id: "inclusionai/ling-2.6-1t", + name: "inclusionAI: Ling-2.6-1T", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.075, + output: 0.625, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "inclusionai/ling-2.6-flash": { + id: "inclusionai/ling-2.6-flash", + name: "inclusionAI: Ling-2.6-flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.01, + output: 0.03, + cacheRead: 0.002, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "inclusionai/ring-2.6-1t": { + id: "inclusionai/ring-2.6-1t", + name: "inclusionAI: Ring-2.6-1T", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.625, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "kwaipilot/kat-coder-pro-v2": { + id: "kwaipilot/kat-coder-pro-v2", + name: "Kwaipilot: KAT-Coder-Pro V2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 80000, + } satisfies Model<"openai-completions">, + "liquid/lfm-2.5-1.2b-thinking:free": { + id: "liquid/lfm-2.5-1.2b-thinking:free", + name: "LiquidAI: LFM2.5-1.2B-Thinking (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "meta-llama/llama-3.1-70b-instruct": { + id: "meta-llama/llama-3.1-70b-instruct", + name: "Meta: Llama 3.1 70B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "meta-llama/llama-3.1-8b-instruct": { + id: "meta-llama/llama-3.1-8b-instruct", + name: "Meta: Llama 3.1 8B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.02, + output: 0.03, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "meta-llama/llama-3.3-70b-instruct": { + id: "meta-llama/llama-3.3-70b-instruct", + name: "Meta: Llama 3.3 70B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.32, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "meta-llama/llama-3.3-70b-instruct:free": { + id: "meta-llama/llama-3.3-70b-instruct:free", + name: "Meta: Llama 3.3 70B Instruct (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "meta-llama/llama-4-maverick": { + id: "meta-llama/llama-4-maverick", + name: "Meta: Llama 4 Maverick", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "meta-llama/llama-4-scout": { + id: "meta-llama/llama-4-scout", + name: "Meta: Llama 4 Scout", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 10000000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "minimax/minimax-m1": { + id: "minimax/minimax-m1", + name: "MiniMax: MiniMax M1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.4, + output: 2.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 40000, + } satisfies Model<"openai-completions">, + "minimax/minimax-m2": { + id: "minimax/minimax-m2", + name: "MiniMax: MiniMax M2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.255, + output: 1, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 196608, + } satisfies Model<"openai-completions">, + "minimax/minimax-m2.1": { + id: "minimax/minimax-m2.1", + name: "MiniMax: MiniMax M2.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.29, + output: 0.95, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 196608, + } satisfies Model<"openai-completions">, + "minimax/minimax-m2.5": { + id: "minimax/minimax-m2.5", + name: "MiniMax: MiniMax M2.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.9, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 196608, + } satisfies Model<"openai-completions">, + "minimax/minimax-m2.7": { + id: "minimax/minimax-m2.7", + name: "MiniMax: MiniMax M2.7", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.24, + output: 0.96, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 196608, + } satisfies Model<"openai-completions">, + "minimax/minimax-m3": { + id: "minimax/minimax-m3", + name: "MiniMax: MiniMax M3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 512000, + } satisfies Model<"openai-completions">, + "mistralai/codestral-2508": { + id: "mistralai/codestral-2508", + name: "Mistral: Codestral 2508", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 0.9, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/devstral-2512": { + id: "mistralai/devstral-2512", + name: "Mistral: Devstral 2 2512", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/ministral-14b-2512": { + id: "mistralai/ministral-14b-2512", + name: "Mistral: Ministral 3 14B 2512", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.2, + output: 0.2, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/ministral-3b-2512": { + id: "mistralai/ministral-3b-2512", + name: "Mistral: Ministral 3 3B 2512", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.1, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/ministral-8b-2512": { + id: "mistralai/ministral-8b-2512", + name: "Mistral: Ministral 3 8B 2512", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-large": { + id: "mistralai/mistral-large", + name: "Mistral Large", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 6, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-large-2407": { + id: "mistralai/mistral-large-2407", + name: "Mistral Large 2407", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 6, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-large-2512": { + id: "mistralai/mistral-large-2512", + name: "Mistral: Mistral Large 3 2512", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-medium-3": { + id: "mistralai/mistral-medium-3", + name: "Mistral: Mistral Medium 3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-medium-3-5": { + id: "mistralai/mistral-medium-3-5", + name: "Mistral: Mistral Medium 3.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-medium-3.1": { + id: "mistralai/mistral-medium-3.1", + name: "Mistral: Mistral Medium 3.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-nemo": { + id: "mistralai/mistral-nemo", + name: "Mistral: Mistral Nemo", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.02, + output: 0.03, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-saba": { + id: "mistralai/mistral-saba", + name: "Mistral: Saba", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.2, + output: 0.6, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-small-2603": { + id: "mistralai/mistral-small-2603", + name: "Mistral: Mistral Small 4", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/mistral-small-3.2-24b-instruct": { + id: "mistralai/mistral-small-3.2-24b-instruct", + name: "Mistral: Mistral Small 3.2 24B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.075, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "mistralai/mixtral-8x22b-instruct": { + id: "mistralai/mixtral-8x22b-instruct", + name: "Mistral: Mixtral 8x22B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 2, + output: 6, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 65536, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "mistralai/voxtral-small-24b-2507": { + id: "mistralai/voxtral-small-24b-2507", + name: "Mistral: Voxtral Small 24B 2507", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2": { + id: "moonshotai/kimi-k2", + name: "MoonshotAI: Kimi K2 0711", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.57, + output: 2.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2-0905": { + id: "moonshotai/kimi-k2-0905", + name: "MoonshotAI: Kimi K2 0905", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2-thinking": { + id: "moonshotai/kimi-k2-thinking", + name: "MoonshotAI: Kimi K2 Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2.5": { + id: "moonshotai/kimi-k2.5", + name: "MoonshotAI: Kimi K2.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.41, + output: 2.06, + cacheRead: 0.07, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2.6": { + id: "moonshotai/kimi-k2.6", + name: "MoonshotAI: Kimi K2.6", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter","requiresReasoningContentOnAssistantMessages":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.66, + output: 3.41, + cacheRead: 0.144, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "moonshotai/kimi-k2.7-code": { + id: "moonshotai/kimi-k2.7-code", + name: "MoonshotAI: Kimi K2.7 Code", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.74, + output: 3.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "nvidia/llama-3.3-nemotron-super-49b-v1.5": { + id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", + name: "NVIDIA: Llama 3.3 Nemotron Super 49B V1.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.4, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-nano-30b-a3b": { + id: "nvidia/nemotron-3-nano-30b-a3b", + name: "NVIDIA: Nemotron 3 Nano 30B A3B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.05, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 228000, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-nano-30b-a3b:free": { + id: "nvidia/nemotron-3-nano-30b-a3b:free", + name: "NVIDIA: Nemotron 3 Nano 30B A3B (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free": { + id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", + name: "NVIDIA: Nemotron 3 Nano Omni (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-super-120b-a12b": { + id: "nvidia/nemotron-3-super-120b-a12b", + name: "NVIDIA: Nemotron 3 Super", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.09, + output: 0.45, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-super-120b-a12b:free": { + id: "nvidia/nemotron-3-super-120b-a12b:free", + name: "NVIDIA: Nemotron 3 Super (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-ultra-550b-a55b": { + id: "nvidia/nemotron-3-ultra-550b-a55b", + name: "NVIDIA: Nemotron 3 Ultra", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 2.2, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-ultra-550b-a55b:free": { + id: "nvidia/nemotron-3-ultra-550b-a55b:free", + name: "NVIDIA: Nemotron 3 Ultra (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-nano-12b-v2-vl:free": { + id: "nvidia/nemotron-nano-12b-v2-vl:free", + name: "NVIDIA: Nemotron Nano 12B 2 VL (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-nano-9b-v2:free": { + id: "nvidia/nemotron-nano-9b-v2:free", + name: "NVIDIA: Nemotron Nano 9B V2 (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-3.5-turbo": { + id: "openai/gpt-3.5-turbo", + name: "OpenAI: GPT-3.5 Turbo", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 16385, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-3.5-turbo-0613": { + id: "openai/gpt-3.5-turbo-0613", + name: "OpenAI: GPT-3.5 Turbo (older v0613)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 4095, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-3.5-turbo-16k": { + id: "openai/gpt-3.5-turbo-16k", + name: "OpenAI: GPT-3.5 Turbo 16k", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 3, + output: 4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 16385, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4": { + id: "openai/gpt-4", + name: "OpenAI: GPT-4", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 30, + output: 60, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8191, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4-turbo": { + id: "openai/gpt-4-turbo", + name: "OpenAI: GPT-4 Turbo", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4-turbo-preview": { + id: "openai/gpt-4-turbo-preview", + name: "OpenAI: GPT-4 Turbo Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4.1": { + id: "openai/gpt-4.1", + name: "OpenAI: GPT-4.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4.1-mini": { + id: "openai/gpt-4.1-mini", + name: "OpenAI: GPT-4.1 Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 1.6, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "openai/gpt-4.1-nano": { + id: "openai/gpt-4.1-nano", + name: "OpenAI: GPT-4.1 Nano", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "openai/gpt-4o": { + id: "openai/gpt-4o", + name: "OpenAI: GPT-4o", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-4o-2024-05-13": { + id: "openai/gpt-4o-2024-05-13", + name: "OpenAI: GPT-4o (2024-05-13)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 5, + output: 15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-4o-2024-08-06": { + id: "openai/gpt-4o-2024-08-06", + name: "OpenAI: GPT-4o (2024-08-06)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-4o-2024-11-20": { + id: "openai/gpt-4o-2024-11-20", + name: "OpenAI: GPT-4o (2024-11-20)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-4o-mini": { + id: "openai/gpt-4o-mini", + name: "OpenAI: GPT-4o-mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-4o-mini-2024-07-18": { + id: "openai/gpt-4o-mini-2024-07-18", + name: "OpenAI: GPT-4o-mini (2024-07-18)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-5": { + id: "openai/gpt-5", + name: "OpenAI: GPT-5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5-codex": { + id: "openai/gpt-5-codex", + name: "OpenAI: GPT-5 Codex", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5-mini": { + id: "openai/gpt-5-mini", + name: "OpenAI: GPT-5 Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5-nano": { + id: "openai/gpt-5-nano", + name: "OpenAI: GPT-5 Nano", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.05, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-5-pro": { + id: "openai/gpt-5-pro", + name: "OpenAI: GPT-5 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 120, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.1": { + id: "openai/gpt-5.1", + name: "OpenAI: GPT-5.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.1-chat": { + id: "openai/gpt-5.1-chat", + name: "OpenAI: GPT-5.1 Chat", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.1-codex": { + id: "openai/gpt-5.1-codex", + name: "OpenAI: GPT-5.1-Codex", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.1-codex-max": { + id: "openai/gpt-5.1-codex-max", + name: "OpenAI: GPT-5.1-Codex-Max", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.1-codex-mini": { + id: "openai/gpt-5.1-codex-mini", + name: "OpenAI: GPT-5.1-Codex-Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.2": { + id: "openai/gpt-5.2", + name: "OpenAI: GPT-5.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.2-chat": { + id: "openai/gpt-5.2-chat", + name: "OpenAI: GPT-5.2 Chat", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-5.2-codex": { + id: "openai/gpt-5.2-codex", + name: "OpenAI: GPT-5.2-Codex", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.2-pro": { + id: "openai/gpt-5.2-pro", + name: "OpenAI: GPT-5.2 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 21, + output: 168, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.3-chat": { + id: "openai/gpt-5.3-chat", + name: "OpenAI: GPT-5.3 Chat", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-5.3-codex": { + id: "openai/gpt-5.3-codex", + name: "OpenAI: GPT-5.3-Codex", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.4": { + id: "openai/gpt-5.4", + name: "OpenAI: GPT-5.4", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.4-mini": { + id: "openai/gpt-5.4-mini", + name: "OpenAI: GPT-5.4 Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.4-nano": { + id: "openai/gpt-5.4-nano", + name: "OpenAI: GPT-5.4 Nano", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.4-pro": { + id: "openai/gpt-5.4-pro", + name: "OpenAI: GPT-5.4 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.5": { + id: "openai/gpt-5.5", + name: "OpenAI: GPT-5.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-5.5-pro": { + id: "openai/gpt-5.5-pro", + name: "OpenAI: GPT-5.5 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","off":null,"minimal":null,"low":null}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-audio": { + id: "openai/gpt-audio", + name: "OpenAI: GPT Audio", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-audio-mini": { + id: "openai/gpt-audio-mini", + name: "OpenAI: GPT Audio Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "openai/gpt-chat-latest": { + id: "openai/gpt-chat-latest", + name: "OpenAI: GPT Chat Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "OpenAI: gpt-oss-120b", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.039, + output: 0.18, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b:free": { + id: "openai/gpt-oss-120b:free", + name: "OpenAI: gpt-oss-120b (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "OpenAI: gpt-oss-20b", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.029, + output: 0.14, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b:free": { + id: "openai/gpt-oss-20b:free", + name: "OpenAI: gpt-oss-20b (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-safeguard-20b": { + id: "openai/gpt-oss-safeguard-20b", + name: "OpenAI: gpt-oss-safeguard-20b", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0.0375, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "openai/o1": { + id: "openai/o1", + name: "OpenAI: o1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o3": { + id: "openai/o3", + name: "OpenAI: o3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o3-deep-research": { + id: "openai/o3-deep-research", + name: "OpenAI: o3 Deep Research", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 40, + cacheRead: 2.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o3-mini": { + id: "openai/o3-mini", + name: "OpenAI: o3 Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o3-mini-high": { + id: "openai/o3-mini-high", + name: "OpenAI: o3 Mini High", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o3-pro": { + id: "openai/o3-pro", + name: "OpenAI: o3 Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o4-mini": { + id: "openai/o4-mini", + name: "OpenAI: o4 Mini", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o4-mini-deep-research": { + id: "openai/o4-mini-deep-research", + name: "OpenAI: o4 Mini Deep Research", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openai/o4-mini-high": { + id: "openai/o4-mini-high", + name: "OpenAI: o4 Mini High", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-completions">, + "openrouter/auto": { + id: "openrouter/auto", + name: "Auto Router", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: -1000000, + output: -1000000, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openrouter/free": { + id: "openrouter/free", + name: "Free Models Router", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "openrouter/fusion": { + id: "openrouter/fusion", + name: "OpenRouter: Fusion", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, + "openrouter/owl-alpha": { + id: "openrouter/owl-alpha", + name: "Owl Alpha", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048756, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "poolside/laguna-m.1": { + id: "poolside/laguna-m.1", + name: "Poolside: Laguna M.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.2, + output: 0.4, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "poolside/laguna-m.1:free": { + id: "poolside/laguna-m.1:free", + name: "Poolside: Laguna M.1 (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "poolside/laguna-xs.2": { + id: "poolside/laguna-xs.2", + name: "Poolside: Laguna XS.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.2, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "poolside/laguna-xs.2:free": { + id: "poolside/laguna-xs.2:free", + name: "Poolside: Laguna XS.2 (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen-2.5-72b-instruct": { + id: "qwen/qwen-2.5-72b-instruct", + name: "Qwen2.5 72B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.36, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen-2.5-7b-instruct": { + id: "qwen/qwen-2.5-7b-instruct", + name: "Qwen: Qwen2.5 7B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.04, + output: 0.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen-plus": { + id: "qwen/qwen-plus", + name: "Qwen: Qwen-Plus", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.26, + output: 0.78, + cacheRead: 0.052, + cacheWrite: 0.325, + }, + contextWindow: 1000000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen-plus-2025-07-28": { + id: "qwen/qwen-plus-2025-07-28", + name: "Qwen: Qwen Plus 0728", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.26, + output: 0.78, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen-plus-2025-07-28:thinking": { + id: "qwen/qwen-plus-2025-07-28:thinking", + name: "Qwen: Qwen Plus 0728 (thinking)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.26, + output: 0.78, + cacheRead: 0, + cacheWrite: 0.325, + }, + contextWindow: 1000000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-14b": { + id: "qwen/qwen3-14b", + name: "Qwen: Qwen3 14B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.24, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131702, + maxTokens: 40960, + } satisfies Model<"openai-completions">, + "qwen/qwen3-235b-a22b": { + id: "qwen/qwen3-235b-a22b", + name: "Qwen: Qwen3 235B A22B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.455, + output: 1.82, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "qwen/qwen3-235b-a22b-2507": { + id: "qwen/qwen3-235b-a22b-2507", + name: "Qwen: Qwen3 235B A22B Instruct 2507", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.09, + output: 0.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen3-235b-a22b-thinking-2507": { + id: "qwen/qwen3-235b-a22b-thinking-2507", + name: "Qwen: Qwen3 235B A22B Thinking 2507", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.1, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3-30b-a3b": { + id: "qwen/qwen3-30b-a3b", + name: "Qwen: Qwen3 30B A3B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.12, + output: 0.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen3-30b-a3b-instruct-2507": { + id: "qwen/qwen3-30b-a3b-instruct-2507", + name: "Qwen: Qwen3 30B A3B Instruct 2507", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.04815, + output: 0.19305, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32000, + } satisfies Model<"openai-completions">, + "qwen/qwen3-30b-a3b-thinking-2507": { + id: "qwen/qwen3-30b-a3b-thinking-2507", + name: "Qwen: Qwen3 30B A3B Thinking 2507", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.08, + output: 0.4, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "qwen/qwen3-32b": { + id: "qwen/qwen3-32b", + name: "Qwen: Qwen3 32B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.08, + output: 0.28, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen3-8b": { + id: "qwen/qwen3-8b", + name: "Qwen: Qwen3 8B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.05, + output: 0.4, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder": { + id: "qwen/qwen3-coder", + name: "Qwen: Qwen3 Coder 480B A35B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.22, + output: 1.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder-30b-a3b-instruct": { + id: "qwen/qwen3-coder-30b-a3b-instruct", + name: "Qwen: Qwen3 Coder 30B A3B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.07, + output: 0.27, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 160000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder-flash": { + id: "qwen/qwen3-coder-flash", + name: "Qwen: Qwen3 Coder Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.195, + output: 0.975, + cacheRead: 0.039, + cacheWrite: 0.24375, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder-next": { + id: "qwen/qwen3-coder-next", + name: "Qwen: Qwen3 Coder Next", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.11, + output: 0.8, + cacheRead: 0.07, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder-plus": { + id: "qwen/qwen3-coder-plus", + name: "Qwen: Qwen3 Coder Plus", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.65, + output: 3.25, + cacheRead: 0.13, + cacheWrite: 0.8125, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3-coder:free": { + id: "qwen/qwen3-coder:free", + name: "Qwen: Qwen3 Coder 480B A35B (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 262000, + } satisfies Model<"openai-completions">, + "qwen/qwen3-max": { + id: "qwen/qwen3-max", + name: "Qwen: Qwen3 Max", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.78, + output: 3.9, + cacheRead: 0.156, + cacheWrite: 0.975, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-max-thinking": { + id: "qwen/qwen3-max-thinking", + name: "Qwen: Qwen3 Max Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.78, + output: 3.9, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-next-80b-a3b-instruct": { + id: "qwen/qwen3-next-80b-a3b-instruct", + name: "Qwen: Qwen3 Next 80B A3B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.09, + output: 1.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen3-next-80b-a3b-instruct:free": { + id: "qwen/qwen3-next-80b-a3b-instruct:free", + name: "Qwen: Qwen3 Next 80B A3B Instruct (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "qwen/qwen3-next-80b-a3b-thinking": { + id: "qwen/qwen3-next-80b-a3b-thinking", + name: "Qwen: Qwen3 Next 80B A3B Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.0975, + output: 0.78, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-235b-a22b-instruct": { + id: "qwen/qwen3-vl-235b-a22b-instruct", + name: "Qwen: Qwen3 VL 235B A22B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.2, + output: 0.88, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-235b-a22b-thinking": { + id: "qwen/qwen3-vl-235b-a22b-thinking", + name: "Qwen: Qwen3 VL 235B A22B Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.26, + output: 2.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-30b-a3b-instruct": { + id: "qwen/qwen3-vl-30b-a3b-instruct", + name: "Qwen: Qwen3 VL 30B A3B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.13, + output: 0.52, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-30b-a3b-thinking": { + id: "qwen/qwen3-vl-30b-a3b-thinking", + name: "Qwen: Qwen3 VL 30B A3B Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.13, + output: 1.56, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-32b-instruct": { + id: "qwen/qwen3-vl-32b-instruct", + name: "Qwen: Qwen3 VL 32B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.104, + output: 0.416, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-8b-instruct": { + id: "qwen/qwen3-vl-8b-instruct", + name: "Qwen: Qwen3 VL 8B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.08, + output: 0.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3-vl-8b-thinking": { + id: "qwen/qwen3-vl-8b-thinking", + name: "Qwen: Qwen3 VL 8B Thinking", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.117, + output: 1.365, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-122b-a10b": { + id: "qwen/qwen3.5-122b-a10b", + name: "Qwen: Qwen3.5-122B-A10B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.26, + output: 2.08, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-27b": { + id: "qwen/qwen3.5-27b", + name: "Qwen: Qwen3.5-27B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.195, + output: 1.56, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-35b-a3b": { + id: "qwen/qwen3.5-35b-a3b", + name: "Qwen: Qwen3.5-35B-A3B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-397b-a17b": { + id: "qwen/qwen3.5-397b-a17b", + name: "Qwen: Qwen3.5 397B A17B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.385, + output: 2.45, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-9b": { + id: "qwen/qwen3.5-9b", + name: "Qwen: Qwen3.5-9B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-flash-02-23": { + id: "qwen/qwen3.5-flash-02-23", + name: "Qwen: Qwen3.5-Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.065, + output: 0.26, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-plus-02-15": { + id: "qwen/qwen3.5-plus-02-15", + name: "Qwen: Qwen3.5 Plus 2026-02-15", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.26, + output: 1.56, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.5-plus-20260420": { + id: "qwen/qwen3.5-plus-20260420", + name: "Qwen: Qwen3.5 Plus 2026-04-20", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.8, + cacheRead: 0, + cacheWrite: 0.375, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.6-27b": { + id: "qwen/qwen3.6-27b", + name: "Qwen: Qwen3.6 27B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.2885, + output: 3.17, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262140, + } satisfies Model<"openai-completions">, + "qwen/qwen3.6-35b-a3b": { + id: "qwen/qwen3.6-35b-a3b", + name: "Qwen: Qwen3.6 35B A3B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "qwen/qwen3.6-flash": { + id: "qwen/qwen3.6-flash", + name: "Qwen: Qwen3.6 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1875, + output: 1.125, + cacheRead: 0, + cacheWrite: 0.234375, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.6-max-preview": { + id: "qwen/qwen3.6-max-preview", + name: "Qwen: Qwen3.6 Max Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.04, + output: 6.24, + cacheRead: 0, + cacheWrite: 1.3, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.6-plus": { + id: "qwen/qwen3.6-plus", + name: "Qwen: Qwen3.6 Plus", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.325, + output: 1.95, + cacheRead: 0, + cacheWrite: 0.40625, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.7-max": { + id: "qwen/qwen3.7-max", + name: "Qwen: Qwen3.7 Max", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.25, + output: 3.75, + cacheRead: 0.25, + cacheWrite: 1.5625, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "qwen/qwen3.7-plus": { + id: "qwen/qwen3.7-plus", + name: "Qwen: Qwen3.7 Plus", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.32, + output: 1.28, + cacheRead: 0.064, + cacheWrite: 0.4, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "rekaai/reka-edge": { + id: "rekaai/reka-edge", + name: "Reka Edge", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 16384, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "relace/relace-search": { + id: "relace/relace-search", + name: "Relace: Relace Search", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "sao10k/l3.1-euryale-70b": { + id: "sao10k/l3.1-euryale-70b", + name: "Sao10K: Llama 3.1 Euryale 70B v2.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.85, + output: 0.85, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "stepfun/step-3.5-flash": { + id: "stepfun/step-3.5-flash", + name: "StepFun: Step 3.5 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.09, + output: 0.3, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "stepfun/step-3.7-flash": { + id: "stepfun/step-3.7-flash", + name: "StepFun: Step 3.7 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.15, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "tencent/hy3-preview": { + id: "tencent/hy3-preview", + name: "Tencent: Hy3 preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.063, + output: 0.21, + cacheRead: 0.021, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "thedrummer/unslopnemo-12b": { + id: "thedrummer/unslopnemo-12b", + name: "TheDrummer: UnslopNemo 12B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "upstage/solar-pro-3": { + id: "upstage/solar-pro-3", + name: "Upstage: Solar Pro 3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "x-ai/grok-4.20": { + id: "x-ai/grok-4.20", + name: "xAI: Grok 4.20", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "x-ai/grok-4.3": { + id: "x-ai/grok-4.3", + name: "xAI: Grok 4.3", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "x-ai/grok-build-0.1": { + id: "x-ai/grok-build-0.1", + name: "xAI: Grok Build 0.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "xiaomi/mimo-v2.5": { + id: "xiaomi/mimo-v2.5", + name: "Xiaomi: MiMo-V2.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "xiaomi/mimo-v2.5-pro": { + id: "xiaomi/mimo-v2.5-pro", + name: "Xiaomi: MiMo-V2.5-Pro", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.0036, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.5": { + id: "z-ai/glm-4.5", + name: "Z.ai: GLM 4.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.5-air": { + id: "z-ai/glm-4.5-air", + name: "Z.ai: GLM 4.5 Air", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.13, + output: 0.85, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.5v": { + id: "z-ai/glm-4.5v", + name: "Z.ai: GLM 4.5V", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 1.8, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 65536, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.6": { + id: "z-ai/glm-4.6", + name: "Z.ai: GLM 4.6", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.43, + output: 1.74, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.6v": { + id: "z-ai/glm-4.6v", + name: "Z.ai: GLM 4.6V", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 0.9, + cacheRead: 0.055, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.7": { + id: "z-ai/glm-4.7", + name: "Z.ai: GLM 4.7", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.4, + output: 1.75, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "z-ai/glm-4.7-flash": { + id: "z-ai/glm-4.7-flash", + name: "Z.ai: GLM 4.7 Flash", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 16384, + } satisfies Model<"openai-completions">, + "z-ai/glm-5": { + id: "z-ai/glm-5", + name: "Z.ai: GLM 5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 1.9, + cacheRead: 0.119, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "z-ai/glm-5-turbo": { + id: "z-ai/glm-5-turbo", + name: "Z.ai: GLM 5 Turbo", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.2, + output: 4, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "z-ai/glm-5.1": { + id: "z-ai/glm-5.1", + name: "Z.ai: GLM 5.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.98, + output: 3.08, + cacheRead: 0.49, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "z-ai/glm-5.2": { + id: "z-ai/glm-5.2", + name: "Z.ai: GLM 5.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text"], + cost: { + input: 0.98, + output: 3.08, + cacheRead: 0.182, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 4096, + } satisfies Model<"openai-completions">, + "z-ai/glm-5v-turbo": { + id: "z-ai/glm-5v-turbo", + name: "Z.ai: GLM 5V Turbo", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.2, + output: 4, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "~anthropic/claude-fable-latest": { + id: "~anthropic/claude-fable-latest", + name: "Anthropic: Claude Fable Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "~anthropic/claude-haiku-latest": { + id: "~anthropic/claude-haiku-latest", + name: "Anthropic Claude Haiku Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, + "~anthropic/claude-opus-latest": { + id: "~anthropic/claude-opus-latest", + name: "Anthropic: Claude Opus Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "~anthropic/claude-sonnet-latest": { + id: "~anthropic/claude-sonnet-latest", + name: "Anthropic Claude Sonnet Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "~google/gemini-flash-latest": { + id: "~google/gemini-flash-latest", + name: "Google Gemini Flash Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0.083333, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "~google/gemini-pro-latest": { + id: "~google/gemini-pro-latest", + name: "Google Gemini Pro Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0.375, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "~moonshotai/kimi-latest": { + id: "~moonshotai/kimi-latest", + name: "MoonshotAI Kimi Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.66, + output: 3.41, + cacheRead: 0.144, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "~openai/gpt-latest": { + id: "~openai/gpt-latest", + name: "OpenAI GPT Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "~openai/gpt-mini-latest": { + id: "~openai/gpt-mini-latest", + name: "OpenAI GPT Mini Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/openrouter.ts b/packages/ai/src/providers/openrouter.ts new file mode 100644 index 00000000..8c3f254d --- /dev/null +++ b/packages/ai/src/providers/openrouter.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { OPENROUTER_MODELS } from "./openrouter.models.ts"; + +export function openrouterProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "openrouter", + name: "OpenRouter", + baseUrl: "https://openrouter.ai/api/v1", + auth: { apiKey: envApiKeyAuth("OpenRouter API key", ["OPENROUTER_API_KEY"]) }, + models: Object.values(OPENROUTER_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/register-builtins.ts b/packages/ai/src/providers/register-builtins.ts deleted file mode 100644 index 8fdcaaf0..00000000 --- a/packages/ai/src/providers/register-builtins.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { clearApiProviders, registerApiProvider } from "../api-registry.ts"; -import type { - Api, - AssistantMessage, - AssistantMessageEvent, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, -} from "../types.ts"; -import { AssistantMessageEventStream } from "../utils/event-stream.ts"; -import type { BedrockOptions } from "./amazon-bedrock.ts"; -import type { AnthropicOptions } from "./anthropic.ts"; -import type { AzureOpenAIResponsesOptions } from "./azure-openai-responses.ts"; -import type { GoogleOptions } from "./google.ts"; -import type { GoogleVertexOptions } from "./google-vertex.ts"; -import type { MistralOptions } from "./mistral.ts"; -import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.ts"; -import type { OpenAICompletionsOptions } from "./openai-completions.ts"; -import type { OpenAIResponsesOptions } from "./openai-responses.ts"; - -interface LazyProviderModule< - TApi extends Api, - TOptions extends StreamOptions, - TSimpleOptions extends SimpleStreamOptions, -> { - stream: (model: Model, context: Context, options?: TOptions) => AsyncIterable; - streamSimple: ( - model: Model, - context: Context, - options?: TSimpleOptions, - ) => AsyncIterable; -} - -interface AnthropicProviderModule { - streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions>; - streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions>; -} - -interface AzureOpenAIResponsesProviderModule { - streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions>; - streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-responses", SimpleStreamOptions>; -} - -interface GoogleProviderModule { - streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions>; - streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleStreamOptions>; -} - -interface GoogleVertexProviderModule { - streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOptions>; - streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStreamOptions>; -} - -interface MistralProviderModule { - streamMistral: StreamFunction<"mistral-conversations", MistralOptions>; - streamSimpleMistral: StreamFunction<"mistral-conversations", SimpleStreamOptions>; -} - -interface OpenAICodexResponsesProviderModule { - streamOpenAICodexResponses: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions>; - streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-responses", SimpleStreamOptions>; -} - -interface OpenAICompletionsProviderModule { - streamOpenAICompletions: StreamFunction<"openai-completions", OpenAICompletionsOptions>; - streamSimpleOpenAICompletions: StreamFunction<"openai-completions", SimpleStreamOptions>; -} - -interface OpenAIResponsesProviderModule { - streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIResponsesOptions>; - streamSimpleOpenAIResponses: StreamFunction<"openai-responses", SimpleStreamOptions>; -} - -interface BedrockProviderModule { - streamBedrock: ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: BedrockOptions, - ) => AsyncIterable; - streamSimpleBedrock: ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: SimpleStreamOptions, - ) => AsyncIterable; -} - -const importNodeOnlyProvider = (specifier: string): Promise => { - const runtimeSpecifier = import.meta.url.endsWith(".js") ? specifier.replace(/\.ts$/, ".js") : specifier; - return import(runtimeSpecifier); -}; - -let anthropicProviderModulePromise: - | Promise> - | undefined; -let azureOpenAIResponsesProviderModulePromise: - | Promise> - | undefined; -let googleProviderModulePromise: - | Promise> - | undefined; -let googleVertexProviderModulePromise: - | Promise> - | undefined; -let mistralProviderModulePromise: - | Promise> - | undefined; -let openAICodexResponsesProviderModulePromise: - | Promise> - | undefined; -let openAICompletionsProviderModulePromise: - | Promise> - | undefined; -let openAIResponsesProviderModulePromise: - | Promise> - | undefined; -let bedrockProviderModuleOverride: - | LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> - | undefined; -let bedrockProviderModulePromise: - | Promise> - | undefined; - -export function setBedrockProviderModule(module: BedrockProviderModule): void { - bedrockProviderModuleOverride = { - stream: module.streamBedrock, - streamSimple: module.streamSimpleBedrock, - }; -} - -function forwardStream(target: AssistantMessageEventStream, source: AsyncIterable): void { - (async () => { - for await (const event of source) { - target.push(event); - } - target.end(); - })(); -} - -function createLazyLoadErrorMessage(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 createLazyStream( - loadModule: () => Promise>, -): StreamFunction { - return (model, context, options) => { - const outer = new AssistantMessageEventStream(); - - loadModule() - .then((module) => { - const inner = module.stream(model, context, options); - forwardStream(outer, inner); - }) - .catch((error) => { - const message = createLazyLoadErrorMessage(model, error); - outer.push({ type: "error", reason: "error", error: message }); - outer.end(message); - }); - - return outer; - }; -} - -function createLazySimpleStream< - TApi extends Api, - TOptions extends StreamOptions, - TSimpleOptions extends SimpleStreamOptions, ->(loadModule: () => Promise>): StreamFunction { - return (model, context, options) => { - const outer = new AssistantMessageEventStream(); - - loadModule() - .then((module) => { - const inner = module.streamSimple(model, context, options); - forwardStream(outer, inner); - }) - .catch((error) => { - const message = createLazyLoadErrorMessage(model, error); - outer.push({ type: "error", reason: "error", error: message }); - outer.end(message); - }); - - return outer; - }; -} - -function loadAnthropicProviderModule(): Promise< - LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions> -> { - anthropicProviderModulePromise ||= import("./anthropic.ts").then((module) => { - const provider = module as AnthropicProviderModule; - return { - stream: provider.streamAnthropic, - streamSimple: provider.streamSimpleAnthropic, - }; - }); - return anthropicProviderModulePromise; -} - -function loadAzureOpenAIResponsesProviderModule(): Promise< - LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions> -> { - azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.ts").then((module) => { - const provider = module as AzureOpenAIResponsesProviderModule; - return { - stream: provider.streamAzureOpenAIResponses, - streamSimple: provider.streamSimpleAzureOpenAIResponses, - }; - }); - return azureOpenAIResponsesProviderModulePromise; -} - -function loadGoogleProviderModule(): Promise< - LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions> -> { - googleProviderModulePromise ||= import("./google.ts").then((module) => { - const provider = module as GoogleProviderModule; - return { - stream: provider.streamGoogle, - streamSimple: provider.streamSimpleGoogle, - }; - }); - return googleProviderModulePromise; -} - -function loadGoogleVertexProviderModule(): Promise< - LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions> -> { - googleVertexProviderModulePromise ||= import("./google-vertex.ts").then((module) => { - const provider = module as GoogleVertexProviderModule; - return { - stream: provider.streamGoogleVertex, - streamSimple: provider.streamSimpleGoogleVertex, - }; - }); - return googleVertexProviderModulePromise; -} - -function loadMistralProviderModule(): Promise< - LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions> -> { - mistralProviderModulePromise ||= import("./mistral.ts").then((module) => { - const provider = module as MistralProviderModule; - return { - stream: provider.streamMistral, - streamSimple: provider.streamSimpleMistral, - }; - }); - return mistralProviderModulePromise; -} - -function loadOpenAICodexResponsesProviderModule(): Promise< - LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions> -> { - openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.ts").then((module) => { - const provider = module as OpenAICodexResponsesProviderModule; - return { - stream: provider.streamOpenAICodexResponses, - streamSimple: provider.streamSimpleOpenAICodexResponses, - }; - }); - return openAICodexResponsesProviderModulePromise; -} - -function loadOpenAICompletionsProviderModule(): Promise< - LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions> -> { - openAICompletionsProviderModulePromise ||= import("./openai-completions.ts").then((module) => { - const provider = module as OpenAICompletionsProviderModule; - return { - stream: provider.streamOpenAICompletions, - streamSimple: provider.streamSimpleOpenAICompletions, - }; - }); - return openAICompletionsProviderModulePromise; -} - -function loadOpenAIResponsesProviderModule(): Promise< - LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions> -> { - openAIResponsesProviderModulePromise ||= import("./openai-responses.ts").then((module) => { - const provider = module as OpenAIResponsesProviderModule; - return { - stream: provider.streamOpenAIResponses, - streamSimple: provider.streamSimpleOpenAIResponses, - }; - }); - return openAIResponsesProviderModulePromise; -} - -function loadBedrockProviderModule(): Promise< - LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> -> { - if (bedrockProviderModuleOverride) { - return Promise.resolve(bedrockProviderModuleOverride); - } - bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.ts").then((module) => { - const provider = module as BedrockProviderModule; - return { - stream: provider.streamBedrock, - streamSimple: provider.streamSimpleBedrock, - }; - }); - return bedrockProviderModulePromise; -} - -export const streamAnthropic = createLazyStream(loadAnthropicProviderModule); -export const streamSimpleAnthropic = createLazySimpleStream(loadAnthropicProviderModule); -export const streamAzureOpenAIResponses = createLazyStream(loadAzureOpenAIResponsesProviderModule); -export const streamSimpleAzureOpenAIResponses = createLazySimpleStream(loadAzureOpenAIResponsesProviderModule); -export const streamGoogle = createLazyStream(loadGoogleProviderModule); -export const streamSimpleGoogle = createLazySimpleStream(loadGoogleProviderModule); -export const streamGoogleVertex = createLazyStream(loadGoogleVertexProviderModule); -export const streamSimpleGoogleVertex = createLazySimpleStream(loadGoogleVertexProviderModule); -export const streamMistral = createLazyStream(loadMistralProviderModule); -export const streamSimpleMistral = createLazySimpleStream(loadMistralProviderModule); -export const streamOpenAICodexResponses = createLazyStream(loadOpenAICodexResponsesProviderModule); -export const streamSimpleOpenAICodexResponses = createLazySimpleStream(loadOpenAICodexResponsesProviderModule); -export const streamOpenAICompletions = createLazyStream(loadOpenAICompletionsProviderModule); -export const streamSimpleOpenAICompletions = createLazySimpleStream(loadOpenAICompletionsProviderModule); -export const streamOpenAIResponses = createLazyStream(loadOpenAIResponsesProviderModule); -export const streamSimpleOpenAIResponses = createLazySimpleStream(loadOpenAIResponsesProviderModule); -const streamBedrockLazy = createLazyStream(loadBedrockProviderModule); -const streamSimpleBedrockLazy = createLazySimpleStream(loadBedrockProviderModule); - -export function registerBuiltInApiProviders(): void { - registerApiProvider({ - api: "anthropic-messages", - stream: streamAnthropic, - streamSimple: streamSimpleAnthropic, - }); - - registerApiProvider({ - api: "openai-completions", - stream: streamOpenAICompletions, - streamSimple: streamSimpleOpenAICompletions, - }); - - registerApiProvider({ - api: "mistral-conversations", - stream: streamMistral, - streamSimple: streamSimpleMistral, - }); - - registerApiProvider({ - api: "openai-responses", - stream: streamOpenAIResponses, - streamSimple: streamSimpleOpenAIResponses, - }); - - registerApiProvider({ - api: "azure-openai-responses", - stream: streamAzureOpenAIResponses, - streamSimple: streamSimpleAzureOpenAIResponses, - }); - - registerApiProvider({ - api: "openai-codex-responses", - stream: streamOpenAICodexResponses, - streamSimple: streamSimpleOpenAICodexResponses, - }); - - registerApiProvider({ - api: "google-generative-ai", - stream: streamGoogle, - streamSimple: streamSimpleGoogle, - }); - - registerApiProvider({ - api: "google-vertex", - stream: streamGoogleVertex, - streamSimple: streamSimpleGoogleVertex, - }); - - registerApiProvider({ - api: "bedrock-converse-stream", - stream: streamBedrockLazy, - streamSimple: streamSimpleBedrockLazy, - }); -} - -export function resetApiProviders(): void { - clearApiProviders(); - registerBuiltInApiProviders(); -} - -registerBuiltInApiProviders(); diff --git a/packages/ai/src/providers/together.models.ts b/packages/ai/src/providers/together.models.ts new file mode 100644 index 00000000..6a261484 --- /dev/null +++ b/packages/ai/src/providers/together.models.ts @@ -0,0 +1,363 @@ +// 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 TOGETHER_MODELS = { + "MiniMaxAI/MiniMax-M2.7": { + id: "MiniMaxAI/MiniMax-M2.7", + name: "MiniMax-M2.7", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "MiniMaxAI/MiniMax-M3": { + id: "MiniMaxAI/MiniMax-M3", + name: "MiniMax-M3", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 524288, + maxTokens: 250000, + } satisfies Model<"openai-completions">, + "Qwen/Qwen2.5-7B-Instruct-Turbo": { + id: "Qwen/Qwen2.5-7B-Instruct-Turbo", + name: "Qwen 2.5 7B Instruct Turbo", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { + id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput", + name: "Qwen3 235B A22B Instruct 2507 FP8", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.2, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-397B-A17B": { + id: "Qwen/Qwen3.5-397B-A17B", + name: "Qwen3.5 397B A17B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 130000, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-9B": { + id: "Qwen/Qwen3.5-9B", + name: "Qwen3.5 9B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.6-Plus": { + id: "Qwen/Qwen3.6-Plus", + name: "Qwen3.6 Plus", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 500000, + } satisfies Model<"openai-completions">, + "Qwen/Qwen3.7-Max": { + id: "Qwen/Qwen3.7-Max", + name: "Qwen3.7 Max", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 1.25, + output: 3.75, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 500000, + } satisfies Model<"openai-completions">, + "deepseek-ai/DeepSeek-V4-Pro": { + id: "deepseek-ai/DeepSeek-V4-Pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null}, + input: ["text"], + cost: { + input: 1.74, + output: 3.48, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, + "essentialai/Rnj-1-Instruct": { + id: "essentialai/Rnj-1-Instruct", + name: "Rnj-1 Instruct", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 32768, + } satisfies Model<"openai-completions">, + "google/gemma-4-31B-it": { + id: "google/gemma-4-31B-it", + name: "Gemma 4 31B Instruct", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.39, + output: 0.97, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "meta-llama/Llama-3.3-70B-Instruct-Turbo": { + id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", + name: "Llama 3.3 70B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.88, + output: 0.88, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.6": { + id: "moonshotai/Kimi-K2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 1.2, + output: 4.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131000, + } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.7-Code": { + id: "moonshotai/Kimi-K2.7-Code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "nvidia/nemotron-3-ultra-550b-a55b": { + id: "nvidia/nemotron-3-ultra-550b-a55b", + name: "Nemotron 3 Ultra 550B A55B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.6, + output: 3.6, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 512300, + maxTokens: 512300, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT OSS 120B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","thinkingFormat":"openai","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null}, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","thinkingFormat":"openai","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null}, + input: ["text"], + cost: { + input: 0.05, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5": { + id: "zai-org/GLM-5", + name: "GLM-5", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5.1": { + id: "zai-org/GLM-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","thinkingFormat":"together","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/together.ts b/packages/ai/src/providers/together.ts new file mode 100644 index 00000000..36631d11 --- /dev/null +++ b/packages/ai/src/providers/together.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { TOGETHER_MODELS } from "./together.models.ts"; + +export function togetherProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "together", + name: "Together", + baseUrl: "https://api.together.ai/v1", + auth: { apiKey: envApiKeyAuth("Together API key", ["TOGETHER_API_KEY"]) }, + models: Object.values(TOGETHER_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/vercel-ai-gateway.models.ts b/packages/ai/src/providers/vercel-ai-gateway.models.ts new file mode 100644 index 00000000..ea65e49c --- /dev/null +++ b/packages/ai/src/providers/vercel-ai-gateway.models.ts @@ -0,0 +1,2899 @@ +// 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 VERCEL_AI_GATEWAY_MODELS = { + "alibaba/qwen-3-14b": { + id: "alibaba/qwen-3-14b", + name: "Qwen3-14B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.12, + output: 0.24, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 40960, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen-3-235b": { + id: "alibaba/qwen-3-235b", + name: "Qwen3 235B A22B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.22, + output: 0.88, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen-3-30b": { + id: "alibaba/qwen-3-30b", + name: "Qwen3-30B-A3B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.12, + output: 0.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 40960, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen-3-32b": { + id: "alibaba/qwen-3-32b", + name: "Qwen 3 32B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.16, + output: 0.64, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen-3.6-max-preview": { + id: "alibaba/qwen-3.6-max-preview", + name: "Qwen 3.6 Max Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.3, + output: 7.8, + cacheRead: 0.26, + cacheWrite: 1.625, + }, + contextWindow: 240000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-235b-a22b-thinking": { + id: "alibaba/qwen3-235b-a22b-thinking", + name: "Qwen3 VL 235B A22B Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-coder": { + id: "alibaba/qwen3-coder", + name: "Qwen3 Coder 480B A35B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0.3, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-coder-30b-a3b": { + id: "alibaba/qwen3-coder-30b-a3b", + name: "Qwen 3 Coder 30B A3B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-coder-next": { + id: "alibaba/qwen3-coder-next", + name: "Qwen3 Coder Next", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-coder-plus": { + id: "alibaba/qwen3-coder-plus", + name: "Qwen3 Coder Plus", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-max": { + id: "alibaba/qwen3-max", + name: "Qwen3 Max", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 1.2, + output: 6, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-max-preview": { + id: "alibaba/qwen3-max-preview", + name: "Qwen3 Max Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 1.2, + output: 6, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-max-thinking": { + id: "alibaba/qwen3-max-thinking", + name: "Qwen 3 Max Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.2, + output: 6, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-next-80b-a3b-instruct": { + id: "alibaba/qwen3-next-80b-a3b-instruct", + name: "Qwen3 Next 80B A3B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-next-80b-a3b-thinking": { + id: "alibaba/qwen3-next-80b-a3b-thinking", + name: "Qwen3 Next 80B A3B Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3-vl-thinking": { + id: "alibaba/qwen3-vl-thinking", + name: "Qwen3 VL 235B A22B Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.5-flash": { + id: "alibaba/qwen3.5-flash", + name: "Qwen 3.5 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.001, + cacheWrite: 0.125, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.5-plus": { + id: "alibaba/qwen3.5-plus", + name: "Qwen 3.5 Plus", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2.4, + cacheRead: 0.04, + cacheWrite: 0.5, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.6-27b": { + id: "alibaba/qwen3.6-27b", + name: "Qwen 3.6 27B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.6-plus": { + id: "alibaba/qwen3.6-plus", + name: "Qwen 3.6 Plus", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.1, + cacheWrite: 0.625, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.7-max": { + id: "alibaba/qwen3.7-max", + name: "Qwen 3.7 Max", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.25, + output: 3.75, + cacheRead: 0.25, + cacheWrite: 1.5625, + }, + contextWindow: 991000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.7-plus": { + id: "alibaba/qwen3.7-plus", + name: "Qwen 3.7 Plus", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 1.6, + cacheRead: 0.08, + cacheWrite: 0.5, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-3-haiku": { + id: "anthropic/claude-3-haiku", + name: "Claude 3 Haiku", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.25, + cacheRead: 0.03, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-3.5-haiku": { + id: "anthropic/claude-3.5-haiku", + name: "Claude 3.5 Haiku", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-haiku-4.5": { + id: "anthropic/claude-haiku-4.5", + name: "Claude Haiku 4.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4": { + id: "anthropic/claude-opus-4", + name: "Claude Opus 4", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.1": { + id: "anthropic/claude-opus-4.1", + name: "Claude Opus 4.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.5": { + id: "anthropic/claude-opus-4.5", + name: "Claude Opus 4.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.6": { + id: "anthropic/claude-opus-4.6", + name: "Claude Opus 4.6", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + thinkingLevelMap: {"xhigh":"max"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.7": { + id: "anthropic/claude-opus-4.7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.8": { + id: "anthropic/claude-opus-4.8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-sonnet-4": { + id: "anthropic/claude-sonnet-4", + name: "Claude Sonnet 4", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-sonnet-4.5": { + id: "anthropic/claude-sonnet-4.5", + name: "Claude Sonnet 4.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "anthropic/claude-sonnet-4.6": { + id: "anthropic/claude-sonnet-4.6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + compat: {"forceAdaptiveThinking":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "arcee-ai/trinity-large-preview": { + id: "arcee-ai/trinity-large-preview", + name: "Trinity Large Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.25, + output: 1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131000, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "arcee-ai/trinity-large-thinking": { + id: "arcee-ai/trinity-large-thinking", + name: "Trinity Large Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.25, + output: 0.9, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262100, + maxTokens: 80000, + } satisfies Model<"anthropic-messages">, + "bytedance/seed-1.6": { + id: "bytedance/seed-1.6", + name: "Seed 1.6", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "cohere/command-a": { + id: "cohere/command-a", + name: "Command A", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 2.5, + output: 10, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 8000, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-r1": { + id: "deepseek/deepseek-r1", + name: "DeepSeek-R1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.35, + output: 5.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v3": { + id: "deepseek/deepseek-v3", + name: "DeepSeek V3 0324", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.27, + output: 1.12, + cacheRead: 0.135, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 163840, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v3.1": { + id: "deepseek/deepseek-v3.1", + name: "DeepSeek V3.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.56, + output: 1.68, + cacheRead: 0.28, + cacheWrite: 0, + }, + contextWindow: 163840, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v3.1-terminus": { + id: "deepseek/deepseek-v3.1-terminus", + name: "DeepSeek V3.1 Terminus", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.27, + output: 1, + cacheRead: 0.135, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v3.2": { + id: "deepseek/deepseek-v3.2", + name: "DeepSeek V3.2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.28, + output: 0.42, + cacheRead: 0.028, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8000, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v3.2-thinking": { + id: "deepseek/deepseek-v3.2-thinking", + name: "DeepSeek V3.2 Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.62, + output: 1.85, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8000, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v4-flash": { + id: "deepseek/deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"anthropic-messages">, + "deepseek/deepseek-v4-pro": { + id: "deepseek/deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.0036, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"anthropic-messages">, + "google/gemini-2.5-flash": { + id: "google/gemini-2.5-flash", + name: "Gemini 2.5 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "google/gemini-2.5-flash-lite": { + id: "google/gemini-2.5-flash-lite", + name: "Gemini 2.5 Flash Lite", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "google/gemini-2.5-pro": { + id: "google/gemini-2.5-pro", + name: "Gemini 2.5 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "google/gemini-3-flash": { + id: "google/gemini-3-flash", + name: "Gemini 3 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65000, + } satisfies Model<"anthropic-messages">, + "google/gemini-3-pro-preview": { + id: "google/gemini-3-pro-preview", + name: "Gemini 3 Pro Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "google/gemini-3.1-flash-lite": { + id: "google/gemini-3.1-flash-lite", + name: "Gemini 3.1 Flash Lite", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65000, + } satisfies Model<"anthropic-messages">, + "google/gemini-3.1-flash-lite-preview": { + id: "google/gemini-3.1-flash-lite-preview", + name: "Gemini 3.1 Flash Lite Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65000, + } satisfies Model<"anthropic-messages">, + "google/gemini-3.1-pro-preview": { + id: "google/gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "google/gemini-3.5-flash": { + id: "google/gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "google/gemma-4-26b-a4b-it": { + id: "google/gemma-4-26b-a4b-it", + name: "Gemma 4 26B A4B IT", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "google/gemma-4-31b-it": { + id: "google/gemma-4-31b-it", + name: "Gemma 4 31B IT", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "inception/mercury-2": { + id: "inception/mercury-2", + name: "Mercury 2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.25, + output: 0.75, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "inception/mercury-coder-small": { + id: "inception/mercury-coder-small", + name: "Mercury Coder Small Beta", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.25, + output: 1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "kwaipilot/kat-coder-pro-v2": { + id: "kwaipilot/kat-coder-pro-v2", + name: "Kat Coder Pro V2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "meituan/longcat-flash-chat": { + id: "meituan/longcat-flash-chat", + name: "LongCat Flash Chat", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "meta/llama-3.1-70b": { + id: "meta/llama-3.1-70b", + name: "Llama 3.1 70B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.72, + output: 0.72, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-3.1-8b": { + id: "meta/llama-3.1-8b", + name: "Llama 3.1 8B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.22, + output: 0.22, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-3.2-11b": { + id: "meta/llama-3.2-11b", + name: "Llama 3.2 11B Vision Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.16, + output: 0.16, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-3.2-90b": { + id: "meta/llama-3.2-90b", + name: "Llama 3.2 90B Vision Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.72, + output: 0.72, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-3.3-70b": { + id: "meta/llama-3.3-70b", + name: "Llama 3.3 70B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.72, + output: 0.72, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-4-maverick": { + id: "meta/llama-4-maverick", + name: "Llama 4 Maverick 17B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.24, + output: 0.97, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "meta/llama-4-scout": { + id: "meta/llama-4-scout", + name: "Llama 4 Scout 17B Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.66, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2": { + id: "minimax/minimax-m2", + name: "MiniMax M2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.03, + cacheWrite: 0.375, + }, + contextWindow: 205000, + maxTokens: 205000, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.1": { + id: "minimax/minimax-m2.1", + name: "MiniMax M2.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.03, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.1-lightning": { + id: "minimax/minimax-m2.1-lightning", + name: "MiniMax M2.1 Lightning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 2.4, + cacheRead: 0.03, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.5": { + id: "minimax/minimax-m2.5", + name: "MiniMax M2.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.03, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.5-highspeed": { + id: "minimax/minimax-m2.5-highspeed", + name: "MiniMax M2.5 High Speed", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.03, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.7": { + id: "minimax/minimax-m2.7", + name: "MiniMax M2.7", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m2.7-highspeed": { + id: "minimax/minimax-m2.7-highspeed", + name: "MiniMax M2.7 High Speed", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + contextWindow: 204800, + maxTokens: 131100, + } satisfies Model<"anthropic-messages">, + "minimax/minimax-m3": { + id: "minimax/minimax-m3", + name: "MiniMax M3", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, + "mistral/codestral": { + id: "mistral/codestral", + name: "Mistral Codestral", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 0.9, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "mistral/devstral-2": { + id: "mistral/devstral-2", + name: "Devstral 2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "mistral/devstral-small": { + id: "mistral/devstral-small", + name: "Devstral Small 1.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "mistral/devstral-small-2": { + id: "mistral/devstral-small-2", + name: "Devstral Small 2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "mistral/ministral-3b": { + id: "mistral/ministral-3b", + name: "Ministral 3B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "mistral/ministral-8b": { + id: "mistral/ministral-8b", + name: "Ministral 8B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "mistral/mistral-medium": { + id: "mistral/mistral-medium", + name: "Mistral Medium 3.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "mistral/mistral-medium-3.5": { + id: "mistral/mistral-medium-3.5", + name: "Mistral Medium Latest", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "mistral/mistral-nemo": { + id: "mistral/mistral-nemo", + name: "Mistral Nemo 12B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "mistral/mistral-small": { + id: "mistral/mistral-small", + name: "Mistral Small", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "mistral/pixtral-12b": { + id: "mistral/pixtral-12b", + name: "Pixtral 12B 2409", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.15, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "mistral/pixtral-large": { + id: "mistral/pixtral-large", + name: "Pixtral Large", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2": { + id: "moonshotai/kimi-k2", + name: "Kimi K2 Instruct", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text"], + cost: { + input: 0.57, + output: 2.3, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2-thinking": { + id: "moonshotai/kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.5, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262114, + maxTokens: 262114, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2.5": { + id: "moonshotai/kimi-k2.5", + name: "Kimi K2.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262114, + maxTokens: 262114, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2.6": { + id: "moonshotai/kimi-k2.6", + name: "Kimi K2.6", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2.7-code": { + id: "moonshotai/kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2.7-code-highspeed": { + id: "moonshotai/kimi-k2.7-code-highspeed", + name: "Kimi K2.7 Code High Speed", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.9, + output: 8, + cacheRead: 0.38, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "nvidia/nemotron-3-super-120b-a12b": { + id: "nvidia/nemotron-3-super-120b-a12b", + name: "NVIDIA Nemotron 3 Super 120B A12B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.15, + output: 0.65, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "nvidia/nemotron-3-ultra-550b-a55b": { + id: "nvidia/nemotron-3-ultra-550b-a55b", + name: "Nemotron 3 Ultra", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65000, + } satisfies Model<"anthropic-messages">, + "nvidia/nemotron-nano-12b-v2-vl": { + id: "nvidia/nemotron-nano-12b-v2-vl", + name: "Nvidia Nemotron Nano 12B V2 VL", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.2, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "nvidia/nemotron-nano-9b-v2": { + id: "nvidia/nemotron-nano-9b-v2", + name: "Nvidia Nemotron Nano 9B V2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.23, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4-turbo": { + id: "openai/gpt-4-turbo", + name: "GPT-4 Turbo", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4.1": { + id: "openai/gpt-4.1", + name: "GPT-4.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4.1-mini": { + id: "openai/gpt-4.1-mini", + name: "GPT-4.1 mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.4, + output: 1.6, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4.1-nano": { + id: "openai/gpt-4.1-nano", + name: "GPT-4.1 nano", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1047576, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4o": { + id: "openai/gpt-4o", + name: "GPT-4o", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-4o-mini": { + id: "openai/gpt-4o-mini", + name: "GPT-4o mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5": { + id: "openai/gpt-5", + name: "GPT-5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5-chat": { + id: "openai/gpt-5-chat", + name: "GPT 5 Chat", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5-codex": { + id: "openai/gpt-5-codex", + name: "GPT-5-Codex", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5-mini": { + id: "openai/gpt-5-mini", + name: "GPT-5 mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5-nano": { + id: "openai/gpt-5-nano", + name: "GPT-5 nano", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.05, + output: 0.4, + cacheRead: 0.005, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5-pro": { + id: "openai/gpt-5-pro", + name: "GPT-5 pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 120, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 272000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.1-codex": { + id: "openai/gpt-5.1-codex", + name: "GPT-5.1-Codex", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.1-codex-max": { + id: "openai/gpt-5.1-codex-max", + name: "GPT 5.1 Codex Max", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.1-codex-mini": { + id: "openai/gpt-5.1-codex-mini", + name: "GPT 5.1 Codex Mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.1-instant": { + id: "openai/gpt-5.1-instant", + name: "GPT-5.1 Instant", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.1-thinking": { + id: "openai/gpt-5.1-thinking", + name: "GPT 5.1 Thinking", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.2": { + id: "openai/gpt-5.2", + name: "GPT 5.2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.2-chat": { + id: "openai/gpt-5.2-chat", + name: "GPT 5.2 Chat", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.2-codex": { + id: "openai/gpt-5.2-codex", + name: "GPT 5.2 Codex", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.2-pro": { + id: "openai/gpt-5.2-pro", + name: "GPT 5.2 ", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 21, + output: 168, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.3-chat": { + id: "openai/gpt-5.3-chat", + name: "GPT-5.3 Chat", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.3-codex": { + id: "openai/gpt-5.3-codex", + name: "GPT 5.3 Codex", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.4": { + id: "openai/gpt-5.4", + name: "GPT 5.4", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.4-mini": { + id: "openai/gpt-5.4-mini", + name: "GPT 5.4 Mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.75, + output: 4.5, + cacheRead: 0.075, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.4-nano": { + id: "openai/gpt-5.4-nano", + name: "GPT 5.4 Nano", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.25, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.4-pro": { + id: "openai/gpt-5.4-pro", + name: "GPT 5.4 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.5": { + id: "openai/gpt-5.5", + name: "GPT 5.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-5.5-pro": { + id: "openai/gpt-5.5-pro", + name: "GPT 5.5 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","off":null,"minimal":null,"low":null}, + input: ["text", "image"], + cost: { + input: 30, + output: 180, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT OSS 120B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.35, + output: 0.75, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.05, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "openai/gpt-oss-safeguard-20b": { + id: "openai/gpt-oss-safeguard-20b", + name: "GPT OSS Safeguard 20B", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.075, + output: 0.3, + cacheRead: 0.037, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"anthropic-messages">, + "openai/o1": { + id: "openai/o1", + name: "o1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "openai/o3": { + id: "openai/o3", + name: "o3", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "openai/o3-deep-research": { + id: "openai/o3-deep-research", + name: "o3-deep-research", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 40, + cacheRead: 2.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "openai/o3-mini": { + id: "openai/o3-mini", + name: "o3-mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "openai/o3-pro": { + id: "openai/o3-pro", + name: "o3 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "openai/o4-mini": { + id: "openai/o4-mini", + name: "o4-mini", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.275, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"anthropic-messages">, + "perplexity/sonar": { + id: "perplexity/sonar", + name: "Sonar", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 127000, + maxTokens: 8000, + } satisfies Model<"anthropic-messages">, + "perplexity/sonar-pro": { + id: "perplexity/sonar-pro", + name: "Sonar Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 8000, + } satisfies Model<"anthropic-messages">, + "sakana/fugu-ultra": { + id: "sakana/fugu-ultra", + name: "Fugu Ultra", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, + "stepfun/step-3.5-flash": { + id: "stepfun/step-3.5-flash", + name: "StepFun 3.5 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.09, + output: 0.3, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 262114, + maxTokens: 262114, + } satisfies Model<"anthropic-messages">, + "stepfun/step-3.7-flash": { + id: "stepfun/step-3.7-flash", + name: "Step 3.7 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.2, + output: 1.15, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.1-fast-non-reasoning": { + id: "xai/grok-4.1-fast-non-reasoning", + name: "Grok 4.1 Fast Non-Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.2, + output: 0.5, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.1-fast-reasoning": { + id: "xai/grok-4.1-fast-reasoning", + name: "Grok 4.1 Fast Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.2, + output: 0.5, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-multi-agent": { + id: "xai/grok-4.20-multi-agent", + name: "Grok 4.20 Multi-Agent", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-multi-agent-beta": { + id: "xai/grok-4.20-multi-agent-beta", + name: "Grok 4.20 Multi Agent Beta", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-non-reasoning": { + id: "xai/grok-4.20-non-reasoning", + name: "Grok 4.20 Non-Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-non-reasoning-beta": { + id: "xai/grok-4.20-non-reasoning-beta", + name: "Grok 4.20 Beta Non-Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: false, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-reasoning": { + id: "xai/grok-4.20-reasoning", + name: "Grok 4.20 Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.20-reasoning-beta": { + id: "xai/grok-4.20-reasoning-beta", + name: "Grok 4.20 Beta Reasoning", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 2000000, + maxTokens: 2000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-4.3": { + id: "xai/grok-4.3", + name: "Grok 4.3", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, + "xai/grok-build-0.1": { + id: "xai/grok-build-0.1", + name: "Grok Build 0.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, + "xiaomi/mimo-v2-flash": { + id: "xiaomi/mimo-v2-flash", + name: "MiMo V2 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "xiaomi/mimo-v2-pro": { + id: "xiaomi/mimo-v2-pro", + name: "MiMo V2 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "xiaomi/mimo-v2.5": { + id: "xiaomi/mimo-v2.5", + name: "MiMo M2.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.14, + output: 0.28, + cacheRead: 0.0028, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 131100, + } satisfies Model<"anthropic-messages">, + "xiaomi/mimo-v2.5-pro": { + id: "xiaomi/mimo-v2.5-pro", + name: "MiMo V2.5 Pro", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.435, + output: 0.87, + cacheRead: 0.0036, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.5": { + id: "zai/glm-4.5", + name: "GLM-4.5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 96000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.5-air": { + id: "zai/glm-4.5-air", + name: "GLM 4.5 Air", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.2, + output: 1.1, + cacheRead: 0.03, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 96000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.5v": { + id: "zai/glm-4.5v", + name: "GLM 4.5V", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 1.8, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 66000, + maxTokens: 16000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.6": { + id: "zai/glm-4.6", + name: "GLM 4.6", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0.11, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 96000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.6v": { + id: "zai/glm-4.6v", + name: "GLM-4.6V", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.3, + output: 0.9, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 24000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.6v-flash": { + id: "zai/glm-4.6v-flash", + name: "GLM-4.6V-Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 24000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.7": { + id: "zai/glm-4.7", + name: "GLM 4.7", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 2.25, + output: 2.75, + cacheRead: 2.25, + cacheWrite: 0, + }, + contextWindow: 131000, + maxTokens: 40000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.7-flash": { + id: "zai/glm-4.7-flash", + name: "GLM 4.7 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.07, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131000, + } satisfies Model<"anthropic-messages">, + "zai/glm-4.7-flashx": { + id: "zai/glm-4.7-flashx", + name: "GLM 4.7 FlashX", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.4, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "zai/glm-5": { + id: "zai/glm-5", + name: "GLM 5", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 202800, + maxTokens: 131100, + } satisfies Model<"anthropic-messages">, + "zai/glm-5-turbo": { + id: "zai/glm-5-turbo", + name: "GLM 5 Turbo", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.2, + output: 4, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 202800, + maxTokens: 131100, + } satisfies Model<"anthropic-messages">, + "zai/glm-5.1": { + id: "zai/glm-5.1", + name: "GLM 5.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 202800, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "zai/glm-5.2": { + id: "zai/glm-5.2", + name: "GLM 5.2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.5, + output: 4.5, + cacheRead: 0.3, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "zai/glm-5v-turbo": { + id: "zai/glm-5v-turbo", + name: "GLM 5V Turbo", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.2, + output: 4, + cacheRead: 0.24, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, +} as const; diff --git a/packages/ai/src/providers/vercel-ai-gateway.ts b/packages/ai/src/providers/vercel-ai-gateway.ts new file mode 100644 index 00000000..3aca0328 --- /dev/null +++ b/packages/ai/src/providers/vercel-ai-gateway.ts @@ -0,0 +1,15 @@ +import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { VERCEL_AI_GATEWAY_MODELS } from "./vercel-ai-gateway.models.ts"; + +export function vercelAIGatewayProvider(): Provider<"anthropic-messages"> { + return createProvider({ + id: "vercel-ai-gateway", + name: "Vercel AI Gateway", + baseUrl: "https://ai-gateway.vercel.sh", + auth: { apiKey: envApiKeyAuth("Vercel AI Gateway API key", ["AI_GATEWAY_API_KEY"]) }, + models: Object.values(VERCEL_AI_GATEWAY_MODELS), + api: anthropicMessagesApi(), + }); +} diff --git a/packages/ai/src/providers/xai.models.ts b/packages/ai/src/providers/xai.models.ts new file mode 100644 index 00000000..5ebb3e55 --- /dev/null +++ b/packages/ai/src/providers/xai.models.ts @@ -0,0 +1,133 @@ +// 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 XAI_MODELS = { + "grok-3": { + id: "grok-3", + name: "Grok 3", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: false, + input: ["text"], + cost: { + input: 3, + output: 15, + cacheRead: 0.75, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "grok-3-fast": { + id: "grok-3-fast", + name: "Grok 3 Fast", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: false, + input: ["text"], + cost: { + input: 5, + output: 25, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"openai-completions">, + "grok-4.20-0309-non-reasoning": { + id: "grok-4.20-0309-non-reasoning", + name: "Grok 4.20 (Non-Reasoning)", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: false, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, + "grok-4.20-0309-reasoning": { + id: "grok-4.20-0309-reasoning", + name: "Grok 4.20 (Reasoning)", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, + "grok-4.3": { + id: "grok-4.3", + name: "Grok 4.3", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 2.5, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, + "grok-build-0.1": { + id: "grok-build-0.1", + name: "Grok Build 0.1", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 2, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "grok-code-fast-1": { + id: "grok-code-fast-1", + name: "Grok Code Fast 1", + api: "openai-completions", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: false, + input: ["text"], + cost: { + input: 0.2, + output: 1.5, + cacheRead: 0.02, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 8192, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/xai.ts b/packages/ai/src/providers/xai.ts new file mode 100644 index 00000000..3373fbf5 --- /dev/null +++ b/packages/ai/src/providers/xai.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { XAI_MODELS } from "./xai.models.ts"; + +export function xaiProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "xai", + name: "xAI", + baseUrl: "https://api.x.ai/v1", + auth: { apiKey: envApiKeyAuth("xAI API key", ["XAI_API_KEY"]) }, + models: Object.values(XAI_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/xiaomi-token-plan-ams.models.ts b/packages/ai/src/providers/xiaomi-token-plan-ams.models.ts new file mode 100644 index 00000000..fec90428 --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-ams.models.ts @@ -0,0 +1,97 @@ +// 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 XIAOMI_TOKEN_PLAN_AMS_MODELS = { + "mimo-v2-omni": { + id: "mimo-v2-omni", + name: "MiMo-V2-Omni", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2-pro": { + id: "mimo-v2-pro", + name: "MiMo-V2-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5": { + id: "mimo-v2.5", + name: "MiMo-V2.5", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro": { + id: "mimo-v2.5-pro", + name: "MiMo-V2.5-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/xiaomi-token-plan-ams.ts b/packages/ai/src/providers/xiaomi-token-plan-ams.ts new file mode 100644 index 00000000..017aa671 --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-ams.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { XIAOMI_TOKEN_PLAN_AMS_MODELS } from "./xiaomi-token-plan-ams.models.ts"; + +export function xiaomiTokenPlanAmsProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "xiaomi-token-plan-ams", + name: "Xiaomi Token Plan AMS", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + auth: { apiKey: envApiKeyAuth("Xiaomi Token Plan AMS API key", ["XIAOMI_TOKEN_PLAN_AMS_API_KEY"]) }, + models: Object.values(XIAOMI_TOKEN_PLAN_AMS_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/xiaomi-token-plan-cn.models.ts b/packages/ai/src/providers/xiaomi-token-plan-cn.models.ts new file mode 100644 index 00000000..9932fefa --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-cn.models.ts @@ -0,0 +1,97 @@ +// 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 XIAOMI_TOKEN_PLAN_CN_MODELS = { + "mimo-v2-omni": { + id: "mimo-v2-omni", + name: "MiMo-V2-Omni", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2-pro": { + id: "mimo-v2-pro", + name: "MiMo-V2-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5": { + id: "mimo-v2.5", + name: "MiMo-V2.5", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro": { + id: "mimo-v2.5-pro", + name: "MiMo-V2.5-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/xiaomi-token-plan-cn.ts b/packages/ai/src/providers/xiaomi-token-plan-cn.ts new file mode 100644 index 00000000..f7ab14fa --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-cn.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { XIAOMI_TOKEN_PLAN_CN_MODELS } from "./xiaomi-token-plan-cn.models.ts"; + +export function xiaomiTokenPlanCnProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "xiaomi-token-plan-cn", + name: "Xiaomi Token Plan CN", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + auth: { apiKey: envApiKeyAuth("Xiaomi Token Plan CN API key", ["XIAOMI_TOKEN_PLAN_CN_API_KEY"]) }, + models: Object.values(XIAOMI_TOKEN_PLAN_CN_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/xiaomi-token-plan-sgp.models.ts b/packages/ai/src/providers/xiaomi-token-plan-sgp.models.ts new file mode 100644 index 00000000..dd248921 --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-sgp.models.ts @@ -0,0 +1,97 @@ +// 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 XIAOMI_TOKEN_PLAN_SGP_MODELS = { + "mimo-v2-omni": { + id: "mimo-v2-omni", + name: "MiMo-V2-Omni", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2-pro": { + id: "mimo-v2-pro", + name: "MiMo-V2-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5": { + id: "mimo-v2.5", + name: "MiMo-V2.5", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro": { + id: "mimo-v2.5-pro", + name: "MiMo-V2.5-Pro", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/xiaomi-token-plan-sgp.ts b/packages/ai/src/providers/xiaomi-token-plan-sgp.ts new file mode 100644 index 00000000..e3762057 --- /dev/null +++ b/packages/ai/src/providers/xiaomi-token-plan-sgp.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { XIAOMI_TOKEN_PLAN_SGP_MODELS } from "./xiaomi-token-plan-sgp.models.ts"; + +export function xiaomiTokenPlanSgpProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "xiaomi-token-plan-sgp", + name: "Xiaomi Token Plan SGP", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + auth: { apiKey: envApiKeyAuth("Xiaomi Token Plan SGP API key", ["XIAOMI_TOKEN_PLAN_SGP_API_KEY"]) }, + models: Object.values(XIAOMI_TOKEN_PLAN_SGP_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/xiaomi.models.ts b/packages/ai/src/providers/xiaomi.models.ts new file mode 100644 index 00000000..23ec9d55 --- /dev/null +++ b/packages/ai/src/providers/xiaomi.models.ts @@ -0,0 +1,115 @@ +// 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 XIAOMI_MODELS = { + "mimo-v2-flash": { + id: "mimo-v2-flash", + name: "MiMo-V2-Flash", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.3, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + "mimo-v2-omni": { + id: "mimo-v2-omni", + name: "MiMo-V2-Omni", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2-pro": { + id: "mimo-v2-pro", + name: "MiMo-V2-Pro", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5": { + id: "mimo-v2.5", + name: "MiMo-V2.5", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.4, + output: 2, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro": { + id: "mimo-v2.5-pro", + name: "MiMo-V2.5-Pro", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1, + output: 3, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/xiaomi.ts b/packages/ai/src/providers/xiaomi.ts new file mode 100644 index 00000000..5abf5169 --- /dev/null +++ b/packages/ai/src/providers/xiaomi.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { XIAOMI_MODELS } from "./xiaomi.models.ts"; + +export function xiaomiProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "xiaomi", + name: "Xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + auth: { apiKey: envApiKeyAuth("Xiaomi API key", ["XIAOMI_API_KEY"]) }, + models: Object.values(XIAOMI_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/zai-coding-cn.models.ts b/packages/ai/src/providers/zai-coding-cn.models.ts new file mode 100644 index 00000000..cd13ebe3 --- /dev/null +++ b/packages/ai/src/providers/zai-coding-cn.models.ts @@ -0,0 +1,116 @@ +// 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 ZAI_CODING_CN_MODELS = { + "glm-4.5-air": { + id: "glm-4.5-air", + name: "GLM-4.5-Air", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "glm-4.7": { + id: "glm-4.7", + name: "GLM-4.7", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5-turbo": { + id: "glm-5-turbo", + name: "GLM-5-Turbo", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5.1": { + id: "glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5v-turbo": { + id: "glm-5v-turbo", + name: "GLM-5V-Turbo", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/zai-coding-cn.ts b/packages/ai/src/providers/zai-coding-cn.ts new file mode 100644 index 00000000..2f15a6ca --- /dev/null +++ b/packages/ai/src/providers/zai-coding-cn.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { ZAI_CODING_CN_MODELS } from "./zai-coding-cn.models.ts"; + +export function zaiCodingCnProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "zai-coding-cn", + name: "Z.AI Coding CN", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + auth: { apiKey: envApiKeyAuth("Z.AI Coding CN API key", ["ZAI_CODING_CN_API_KEY"]) }, + models: Object.values(ZAI_CODING_CN_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/providers/zai.models.ts b/packages/ai/src/providers/zai.models.ts new file mode 100644 index 00000000..ba7f55c4 --- /dev/null +++ b/packages/ai/src/providers/zai.models.ts @@ -0,0 +1,116 @@ +// 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 ZAI_MODELS = { + "glm-4.5-air": { + id: "glm-4.5-air", + name: "GLM-4.5-Air", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "glm-4.7": { + id: "glm-4.7", + name: "GLM-4.7", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5-turbo": { + id: "glm-5-turbo", + name: "GLM-5-Turbo", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5.1": { + id: "glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5v-turbo": { + id: "glm-5v-turbo", + name: "GLM-5V-Turbo", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, +} as const; diff --git a/packages/ai/src/providers/zai.ts b/packages/ai/src/providers/zai.ts new file mode 100644 index 00000000..85401066 --- /dev/null +++ b/packages/ai/src/providers/zai.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { ZAI_MODELS } from "./zai.models.ts"; + +export function zaiProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "zai", + name: "Z.AI", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + auth: { apiKey: envApiKeyAuth("Z.AI API key", ["ZAI_API_KEY"]) }, + models: Object.values(ZAI_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts deleted file mode 100644 index 3f333d9e..00000000 --- a/packages/ai/src/stream.ts +++ /dev/null @@ -1,74 +0,0 @@ -import "./providers/register-builtins.ts"; - -import { getApiProvider } from "./api-registry.ts"; -import { getEnvApiKey } from "./env-api-keys.ts"; -import type { - Api, - AssistantMessage, - AssistantMessageEventStream, - Context, - Model, - ProviderStreamOptions, - SimpleStreamOptions, - StreamOptions, -} from "./types.ts"; - -export { getEnvApiKey } from "./env-api-keys.ts"; - -function hasExplicitApiKey(apiKey: string | undefined): apiKey is string { - return typeof apiKey === "string" && apiKey.trim().length > 0; -} - -function withEnvApiKey( - model: Model, - options: TOptions | undefined, -): TOptions | undefined { - if (hasExplicitApiKey(options?.apiKey)) return options; - const apiKey = getEnvApiKey(model.provider, options?.env); - if (!apiKey) return options; - return { ...options, apiKey } as TOptions; -} - -function resolveApiProvider(api: Api) { - const provider = getApiProvider(api); - if (!provider) { - throw new Error(`No API provider registered for api: ${api}`); - } - return provider; -} - -export function stream( - model: Model, - context: Context, - options?: ProviderStreamOptions, -): AssistantMessageEventStream { - const provider = resolveApiProvider(model.api); - return provider.stream(model, context, withEnvApiKey(model, options) as StreamOptions); -} - -export async function complete( - model: Model, - context: Context, - options?: ProviderStreamOptions, -): Promise { - const s = stream(model, context, options); - return s.result(); -} - -export function streamSimple( - model: Model, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream { - const provider = resolveApiProvider(model.api); - return provider.streamSimple(model, context, withEnvApiKey(model, options)); -} - -export async function completeSimple( - model: Model, - context: Context, - options?: SimpleStreamOptions, -): Promise { - const s = streamSimple(model, context, options); - return s.result(); -} diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index cedac6e8..29449813 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -1,3 +1,12 @@ +import type { AnthropicOptions } from "./api/anthropic-messages.ts"; +import type { AzureOpenAIResponsesOptions } from "./api/azure-openai-responses.ts"; +import type { BedrockOptions } from "./api/bedrock-converse-stream.ts"; +import type { GoogleOptions } from "./api/google-generative-ai.ts"; +import type { GoogleVertexOptions } from "./api/google-vertex.ts"; +import type { MistralOptions } from "./api/mistral-conversations.ts"; +import type { OpenAICodexResponsesOptions } from "./api/openai-codex-responses.ts"; +import type { OpenAICompletionsOptions } from "./api/openai-completions.ts"; +import type { OpenAIResponsesOptions } from "./api/openai-responses.ts"; import type { AssistantMessageDiagnostic } from "./utils/diagnostics.ts"; import type { AssistantMessageEventStream } from "./utils/event-stream.ts"; @@ -56,15 +65,24 @@ export type KnownProvider = | "xiaomi-token-plan-cn" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-sgp"; -export type Provider = KnownProvider | string; +export type ProviderId = KnownProvider | string; export type KnownImagesProvider = "openrouter"; -export type ImagesProvider = KnownImagesProvider | string; +export type ImagesProviderId = KnownImagesProvider | string; export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; export type ModelThinkingLevel = "off" | ThinkingLevel; export type ThinkingLevelMap = Partial>; +export type ChatTemplateKwargValue = + | string + | number + | boolean + | null + | { + $var: "thinking.enabled" | "thinking.effort"; + omitWhenOff?: boolean; + }; /** Token budgets for each thinking level (token-based providers only) */ export interface ThinkingBudgets { @@ -81,6 +99,7 @@ export type Transport = "sse" | "websocket" | "websocket-cached" | "auto"; /** Provider-scoped environment overrides. Values take precedence over process.env. */ export type ProviderEnv = Record; +export type ProviderHeaders = Record; export interface ProviderResponse { status: number; @@ -124,8 +143,9 @@ export interface StreamOptions { * On AWS Bedrock these are injected via a Smithy `build`-step middleware so * they are covered by SigV4 signing; reserved headers (`x-amz-*`, * `authorization`, `host`) are silently ignored to preserve SigV4 / bearer auth. + * A null value suppresses a provider/API default header with the same name. */ - headers?: Record; + headers?: ProviderHeaders; /** * HTTP request timeout in milliseconds for providers/SDKs that support it. * For example, OpenAI and Anthropic SDK clients default to 10 minutes. @@ -166,9 +186,66 @@ export interface StreamOptions { export type ProviderStreamOptions = StreamOptions & Record; +/** + * Maps known APIs to their full provider-specific stream option types. + * Type-only imports from API implementation modules are erased at emit, 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; +} + +/** + * Full stream options for an API. Known APIs resolve to their concrete option + * type; custom API strings fall back to the generic shape. + */ +export type ApiStreamOptions = TApi extends keyof ApiOptionsMap + ? ApiOptionsMap[TApi] + : StreamOptions & Record; + +/** + * The uniform stream contract of an API implementation module: every module + * under `src/api/` exports exactly `stream` and `streamSimple`, so the module + * itself satisfies this interface. Lazy wrappers (`lazyApi()`) and provider + * factories pass these around as values. This is the untyped dispatch shape; + * per-API option typing lives on the implementation modules themselves and on + * `Provider.stream()` via `ApiStreamOptions`. + */ +export interface ProviderStreams { + stream(model: Model, context: Context, options?: StreamOptions): AssistantMessageEventStream; + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; +} + +/** + * The uniform contract of an image-generation API implementation module: + * every image API module under `src/api/` exports exactly `generateImages`, + * so the module itself satisfies this interface. Lazy wrappers and image + * provider factories pass these around as values. + */ +export interface ProviderImages { + generateImages( + model: ImagesModel, + context: ImagesContext, + options?: ImagesOptions, + ): Promise; +} + export interface ImagesOptions { signal?: AbortSignal; apiKey?: string; + /** + * Provider-scoped environment values. These take precedence over process.env for + * provider configuration such as endpoint placeholders and proxy variables. + */ + env?: ProviderEnv; /** * Optional callback for inspecting or replacing provider payloads before sending. * Return undefined to keep the payload unchanged. @@ -181,8 +258,9 @@ export interface ImagesOptions { /** * Optional custom HTTP headers to include in API requests. * Merged with provider defaults; can override default headers. + * A null value suppresses a provider/API default header with the same name. */ - headers?: Record; + headers?: ProviderHeaders; /** * HTTP request timeout in milliseconds for providers/SDKs that support it. */ @@ -300,7 +378,7 @@ export interface AssistantMessage { role: "assistant"; content: (TextContent | ThinkingContent | ToolCall)[]; api: Api; - provider: Provider; + provider: ProviderId; model: string; responseModel?: string; // Concrete `chunk.model` when different from the requested `model` (e.g. OpenRouter `auto` -> `anthropic/...`) responseId?: string; // Provider-specific response/message identifier when the upstream API exposes one @@ -334,7 +412,7 @@ export type ImagesStopReason = "stop" | "error" | "aborted"; export interface AssistantImages { api: ImagesApi; - provider: ImagesProvider; + provider: ImagesProviderId; model: string; output: ImagesOutputContent[]; responseId?: string; @@ -403,7 +481,7 @@ export interface OpenAICompletionsCompat { requiresThinkingAsText?: boolean; /** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */ requiresReasoningContentOnAssistantMessages?: boolean; - /** Format for reasoning/thinking parameter. "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 top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */ + /** Format for reasoning/thinking parameter. "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 top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking and preserve_thinking, "chat-template" uses configurable chat_template_kwargs, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */ thinkingFormat?: | "openai" | "openrouter" @@ -411,9 +489,12 @@ export interface OpenAICompletionsCompat { | "together" | "zai" | "qwen" + | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling"; + /** Kwargs to send as `chat_template_kwargs` when `thinkingFormat` is `chat-template`. Use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values. */ + chatTemplateKwargs?: Record; /** OpenRouter-compatible routing preferences sent as the `provider` request field. */ openRouterRouting?: OpenRouterRouting; /** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */ @@ -580,7 +661,7 @@ export interface Model { id: string; name: string; api: TApi; - provider: Provider; + provider: ProviderId; baseUrl: string; reasoning: boolean; /** @@ -611,6 +692,6 @@ export interface Model { export interface ImagesModel extends Omit, "api" | "provider" | "reasoning" | "contextWindow" | "maxTokens" | "compat"> { api: TApi; - provider: ImagesProvider; + provider: ImagesProviderId; output: ("text" | "image")[]; } diff --git a/packages/ai/src/utils/headers.ts b/packages/ai/src/utils/headers.ts index fae8a036..2d923d28 100644 --- a/packages/ai/src/utils/headers.ts +++ b/packages/ai/src/utils/headers.ts @@ -1,3 +1,5 @@ +import type { ProviderHeaders } from "../types.ts"; + export function headersToRecord(headers: Headers): Record { const result: Record = {}; for (const [key, value] of headers.entries()) { @@ -5,3 +7,12 @@ export function headersToRecord(headers: Headers): Record { } return result; } + +export function providerHeadersToRecord(headers: ProviderHeaders | undefined): Record | undefined { + if (!headers) return undefined; + const result: Record = {}; + for (const [key, value] of Object.entries(headers)) { + if (value !== null) result[key] = value; + } + return Object.keys(result).length > 0 ? result : undefined; +} diff --git a/packages/ai/src/utils/oauth/anthropic.ts b/packages/ai/src/utils/oauth/anthropic.ts index 1b3e4244..591e9cde 100644 --- a/packages/ai/src/utils/oauth/anthropic.ts +++ b/packages/ai/src/utils/oauth/anthropic.ts @@ -6,6 +6,7 @@ */ import type { Server } from "node:http"; +import type { OAuthAuth } from "../../auth/types.ts"; import { getProviderEnvValue } from "../provider-env.ts"; import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts"; import { generatePKCE } from "./pkce.ts"; @@ -379,6 +380,42 @@ export async function refreshAnthropicToken(refreshToken: string): Promise callbacks.notify({ type: "auth_url", url: info.url, instructions: info.instructions }), + onProgress: (message) => callbacks.notify({ type: "progress", message }), + onPrompt: (prompt) => + callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }), + onManualCodeInput: () => + callbacks.prompt({ + type: "manual_code", + message: "Complete login in your browser, or paste the authorization code / redirect URL here:", + placeholder: REDIRECT_URI, + signal: manualAbort.signal, + }), + }); + return { ...credentials, type: "oauth" }; + } finally { + manualAbort.abort(); + } + }, + + async refresh(credential) { + return { ...(await refreshAnthropicToken(credential.refresh)), type: "oauth" }; + }, + + async toAuth(credential) { + return { apiKey: credential.access }; + }, +}; + export const anthropicOAuthProvider: OAuthProviderInterface = { id: "anthropic", name: "Anthropic (Claude Pro/Max)", diff --git a/packages/ai/src/utils/oauth/github-copilot.ts b/packages/ai/src/utils/oauth/github-copilot.ts index 6a27b9d1..111af0ad 100644 --- a/packages/ai/src/utils/oauth/github-copilot.ts +++ b/packages/ai/src/utils/oauth/github-copilot.ts @@ -2,13 +2,15 @@ * GitHub Copilot OAuth flow */ -import { getModels } from "../../models.ts"; +import type { OAuthAuth, OAuthCredential } from "../../auth/types.ts"; +import { GITHUB_COPILOT_MODELS } from "../../providers/github-copilot.models.ts"; import type { Api, Model } from "../../types.ts"; import { pollOAuthDeviceCodeFlow } from "./device-code.ts"; import type { OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.ts"; type CopilotCredentials = OAuthCredentials & { enterpriseUrl?: string; + availableModelIds: string[]; }; const decode = (s: string) => atob(s); @@ -20,6 +22,7 @@ const COPILOT_HEADERS = { "Editor-Plugin-Version": "copilot-chat/0.35.0", "Copilot-Integration-Id": "vscode-chat", } as const; +const COPILOT_API_VERSION = "2026-06-01"; type DeviceCodeResponse = { device_code: string; @@ -88,6 +91,48 @@ export function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: strin return "https://api.individual.githubcopilot.com"; } +function asRecord(value: unknown): Record | undefined { + return value && typeof value === "object" ? (value as Record) : undefined; +} + +function isSelectableCopilotModel(item: Record): boolean { + const policy = asRecord(item.policy); + const capabilities = asRecord(item.capabilities); + const supports = asRecord(capabilities?.supports); + return item.model_picker_enabled === true && policy?.state !== "disabled" && supports?.tool_calls !== false; +} + +function parseAvailableCopilotModelIds(raw: unknown): string[] { + const data = asRecord(raw)?.data; + if (!Array.isArray(data)) { + throw new Error("Invalid Copilot models response"); + } + + const ids: string[] = []; + for (const rawItem of data) { + const item = asRecord(rawItem); + const id = item?.id; + if (typeof id === "string" && item && isSelectableCopilotModel(item)) { + ids.push(id); + } + } + return ids; +} + +async function fetchAvailableGitHubCopilotModelIds(copilotToken: string, enterpriseDomain?: string): Promise { + const baseUrl = getGitHubCopilotBaseUrl(copilotToken, enterpriseDomain); + const raw = await fetchJson(`${baseUrl}/models`, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${copilotToken}`, + ...COPILOT_HEADERS, + "X-GitHub-Api-Version": COPILOT_API_VERSION, + }, + signal: AbortSignal.timeout(5000), + }); + return parseAvailableCopilotModelIds(raw); +} + async function fetchJson(url: string, init: RequestInit): Promise { const response = await fetch(url, init); if (!response.ok) { @@ -201,10 +246,7 @@ async function pollForGitHubAccessToken( }); } -/** - * Refresh GitHub Copilot token - */ -export async function refreshGitHubCopilotToken( +async function refreshGitHubCopilotAccessToken( refreshToken: string, enterpriseDomain?: string, ): Promise { @@ -238,6 +280,20 @@ export async function refreshGitHubCopilotToken( }; } +/** + * Refresh GitHub Copilot token + */ +export async function refreshGitHubCopilotToken( + refreshToken: string, + enterpriseDomain?: string, +): Promise { + const credentials = await refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain); + return { + ...credentials, + availableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain), + }; +} + /** * Enable a model for the user's GitHub Copilot account. * This is required for some models (like Claude, Grok) before they can be used. @@ -273,7 +329,7 @@ async function enableAllGitHubCopilotModels( enterpriseDomain?: string, onProgress?: (model: string, success: boolean) => void, ): Promise { - const models = getModels("github-copilot"); + const models = Object.values(GITHUB_COPILOT_MODELS); await Promise.all( models.map(async (model) => { const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain); @@ -322,14 +378,56 @@ export async function loginGitHubCopilot(options: { }); const githubAccessToken = await pollForGitHubAccessToken(domain, device, options.signal); - const credentials = await refreshGitHubCopilotToken(githubAccessToken, enterpriseDomain ?? undefined); + const credentials = await refreshGitHubCopilotAccessToken(githubAccessToken, enterpriseDomain ?? undefined); // Enable all models after successful login options.onProgress?.("Enabling models..."); await enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined); - return credentials; + + // Fetch availability after policy enable so newly enabled models are included, + // while unavailable models are still filtered out. + return { + ...credentials, + availableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain ?? undefined), + }; } +function copilotEnterpriseDomain(credential: OAuthCredential): string | undefined { + const enterpriseUrl = credential.enterpriseUrl; + if (typeof enterpriseUrl !== "string" || !enterpriseUrl) return undefined; + return normalizeDomain(enterpriseUrl) ?? undefined; +} + +export const githubCopilotOAuth: OAuthAuth = { + name: "GitHub Copilot", + + async login(callbacks) { + const credentials = await loginGitHubCopilot({ + onDeviceCode: (info) => callbacks.notify({ type: "device_code", ...info }), + onPrompt: (prompt) => + callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }), + onProgress: (message) => callbacks.notify({ type: "progress", message }), + signal: callbacks.signal, + }); + return { ...credentials, type: "oauth" }; + }, + + async refresh(credential) { + return { + ...(await refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential))), + type: "oauth", + }; + }, + + /** Per-credential baseUrl from the token's proxy endpoint replaces the old `modifyModels` rewriting. */ + async toAuth(credential) { + return { + apiKey: credential.access, + baseUrl: getGitHubCopilotBaseUrl(credential.access, copilotEnterpriseDomain(credential)), + }; + }, +}; + export const githubCopilotOAuthProvider: OAuthProviderInterface = { id: "github-copilot", name: "GitHub Copilot", @@ -356,6 +454,14 @@ export const githubCopilotOAuthProvider: OAuthProviderInterface = { const creds = credentials as CopilotCredentials; const domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined; const baseUrl = getGitHubCopilotBaseUrl(creds.access, domain); - return models.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m)); + // Older stored Pi auth entries do not have account-specific model IDs yet; + // keep their existing generated-catalog behavior until the next refresh/login. + const availableModelIds = "availableModelIds" in creds ? new Set(creds.availableModelIds) : undefined; + + return models.flatMap((m) => { + if (m.provider !== "github-copilot") return [m]; + if (availableModelIds && !availableModelIds.has(m.id)) return []; + return [{ ...m, baseUrl }]; + }); }, }; diff --git a/packages/ai/src/utils/oauth/load.ts b/packages/ai/src/utils/oauth/load.ts new file mode 100644 index 00000000..11198853 --- /dev/null +++ b/packages/ai/src/utils/oauth/load.ts @@ -0,0 +1,21 @@ +import type { OAuthAuth } from "../../auth/types.ts"; + +/** + * Loads an OAuth flow module through a variable specifier so bundlers cannot + * follow the import into Node-only flow code (`node:http` callback servers, + * `node:crypto` PKCE). The `.ts`/`.js` rewrite keeps the trick working from + * both source and built output. + */ +const importOAuthModule = (specifier: string): Promise => { + const runtimeSpecifier = import.meta.url.endsWith(".js") ? specifier.replace(/\.ts$/, ".js") : specifier; + return import(runtimeSpecifier); +}; + +export const loadAnthropicOAuth = async (): Promise => + ((await importOAuthModule("./anthropic.ts")) as { anthropicOAuth: OAuthAuth }).anthropicOAuth; + +export const loadOpenAICodexOAuth = async (): Promise => + ((await importOAuthModule("./openai-codex.ts")) as { openaiCodexOAuth: OAuthAuth }).openaiCodexOAuth; + +export const loadGitHubCopilotOAuth = async (): Promise => + ((await importOAuthModule("./github-copilot.ts")) as { githubCopilotOAuth: OAuthAuth }).githubCopilotOAuth; diff --git a/packages/ai/src/utils/oauth/openai-codex.ts b/packages/ai/src/utils/oauth/openai-codex.ts index a5103c39..a2f7cd00 100644 --- a/packages/ai/src/utils/oauth/openai-codex.ts +++ b/packages/ai/src/utils/oauth/openai-codex.ts @@ -17,6 +17,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } +import type { OAuthAuth } from "../../auth/types.ts"; import { getProviderEnvValue } from "../provider-env.ts"; import { pollOAuthDeviceCodeFlow } from "./device-code.ts"; import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts"; @@ -561,6 +562,62 @@ export async function refreshOpenAICodexToken(refreshToken: string): Promise callbacks.notify({ type: "device_code", ...info }), + signal: callbacks.signal, + }); + return { ...credentials, type: "oauth" }; + } + if (method !== OPENAI_CODEX_BROWSER_LOGIN_METHOD) { + throw new Error(`Unknown OpenAI Codex login method: ${method}`); + } + + // The manual_code prompt races the local callback server; abort it once + // the flow settles so the UI can dismiss the pending input. + const manualAbort = new AbortController(); + try { + const credentials = await loginOpenAICodex({ + onAuth: (info) => callbacks.notify({ type: "auth_url", url: info.url, instructions: info.instructions }), + onProgress: (message) => callbacks.notify({ type: "progress", message }), + onPrompt: (prompt) => + callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }), + onManualCodeInput: () => + callbacks.prompt({ + type: "manual_code", + message: "Complete login in your browser, or paste the authorization code / redirect URL here:", + placeholder: REDIRECT_URI, + signal: manualAbort.signal, + }), + }); + return { ...credentials, type: "oauth" }; + } finally { + manualAbort.abort(); + } + }, + + async refresh(credential) { + return { ...(await refreshOpenAICodexToken(credential.refresh)), type: "oauth" }; + }, + + async toAuth(credential) { + return { apiKey: credential.access }; + }, +}; + export const openaiCodexOAuthProvider: OAuthProviderInterface = { id: "openai-codex", name: "ChatGPT Plus/Pro (Codex Subscription)", diff --git a/packages/ai/src/utils/retry.ts b/packages/ai/src/utils/retry.ts new file mode 100644 index 00000000..d3a95a0a --- /dev/null +++ b/packages/ai/src/utils/retry.ts @@ -0,0 +1,96 @@ +import type { AssistantMessage } from "../types.ts"; + +function buildProviderErrorPattern(patterns: readonly string[]): RegExp { + return new RegExp(patterns.join("|"), "i"); +} + +const NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([ + // OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's + // Zen API. These are subscription/account limits, not transient throttles. + "GoUsageLimitError", + "FreeUsageLimitError", + + // OpenCode Go subscription-limit text asks users to enable available-balance + // usage after rolling/weekly/monthly limits are reached. + "Monthly usage limit reached", + "available balance", + + // Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's + // quota/billing error code; the other strings cover common gateway wording. + "insufficient_quota", + "out of budget", + "quota exceeded", + "billing", +]); + +const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([ + // Generic provider load, HTTP status, and server-side transient failures. + "overloaded", + "rate.?limit", + "too many requests", + "429", + "500", + "502", + "503", + "504", + "service.?unavailable", + "server.?error", + "internal.?error", + + // Wrapper/provider text for transient upstream failures, including OpenRouter + // "Provider returned error" responses (#2264). + "provider.?returned.?error", + + // Network, proxy, and fetch transport failures. This includes OpenAI Codex + // raw-fetch failures such as "upstream connect", "connection refused", and + // "reset before headers" (#733), plus OpenRouter connection drops (#3317). + "network.?error", + "connection.?error", + "connection.?refused", + "connection.?lost", + "other side closed", + "fetch failed", + "upstream.?connect", + "reset before headers", + "socket hang up", + "timed? out", + "timeout", + "terminated", + + // WebSocket transports can report close/error text instead of HTTP/fetch text. + "websocket.?closed", + "websocket.?error", + + // Premature stream endings from SDKs and transports. Anthropic can throw + // "stream ended without ..." and "Anthropic stream ended before message_stop" + // (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594). + "ended without", + "stream ended before message_stop", + "http2 request did not get a response", + + // Provider-requested retry delay cap failures should flow through the outer + // retry policy so callers can surface/abort the backoff (#1123). + "retry delay", + + // Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock + // stream exceptions (#6019). + "you can retry your request", + "try your request again", + "please retry your request", +]); + +/** + * Classifies whether a failed assistant message looks like a transient provider + * or transport error, so callers can decide if the last assistant turn should be + * restarted. + * + * This does not implement retry policy. Callers should first handle context + * overflow separately, then apply their own retry budget, backoff, and reporting + * before restarting the assistant turn. + */ +export function isRetryableAssistantError(message: AssistantMessage): boolean { + if (message.stopReason !== "error" || !message.errorMessage) return false; + const errorMessage = message.errorMessage; + if (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false; + return RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage); +} diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 27c274aa..e4424c5e 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete, stream } from "../src/stream.ts"; +import { complete, getModel, stream } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/anthropic-adaptive-thinking-models.test.ts b/packages/ai/test/anthropic-adaptive-thinking-models.test.ts index 18023e11..8d99ff18 100644 --- a/packages/ai/test/anthropic-adaptive-thinking-models.test.ts +++ b/packages/ai/test/anthropic-adaptive-thinking-models.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModels, getProviders } from "../src/models.ts"; +import { getModels, getProviders } from "../src/compat.ts"; import type { Api, Model } from "../src/types.ts"; const EXPECTED_CURRENT_ADAPTIVE_THINKING_MODELS = [ diff --git a/packages/ai/test/anthropic-cache-write-1h-cost.test.ts b/packages/ai/test/anthropic-cache-write-1h-cost.test.ts index f9523b40..13745e9b 100644 --- a/packages/ai/test/anthropic-cache-write-1h-cost.test.ts +++ b/packages/ai/test/anthropic-cache-write-1h-cost.test.ts @@ -1,7 +1,7 @@ import type Anthropic from "@anthropic-ai/sdk"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; +import { getModel } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; function createSseResponse(events: Array<{ event: string; data: string }>): Response { diff --git a/packages/ai/test/anthropic-eager-tool-input-compat.test.ts b/packages/ai/test/anthropic-eager-tool-input-compat.test.ts index 22ac0e59..c53c9d6b 100644 --- a/packages/ai/test/anthropic-eager-tool-input-compat.test.ts +++ b/packages/ai/test/anthropic-eager-tool-input-compat.test.ts @@ -2,7 +2,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht import type { AddressInfo } from "node:net"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; import type { Context, Model, Tool } from "../src/types.ts"; interface CapturedRequest { diff --git a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts index 2483e1a4..680f0648 100644 --- a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts +++ b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts @@ -1,8 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { complete, getModels, getProviders } from "../src/compat.ts"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { getModels, getProviders } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts index 69e58e27..f5c88368 100644 --- a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts +++ b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/stream.ts"; +import { streamSimple } from "../src/compat.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; interface AnthropicPayload { diff --git a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts index e797c3a9..6629782f 100644 --- a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts +++ b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts index 2b7667d7..042d3536 100644 --- a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts +++ b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete, getModels, getProviders } from "../src/compat.ts"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { getModels, getProviders } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-oauth.test.ts b/packages/ai/test/anthropic-oauth.test.ts index 36585a5f..ae3ae093 100644 --- a/packages/ai/test/anthropic-oauth.test.ts +++ b/packages/ai/test/anthropic-oauth.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { loginAnthropic, refreshAnthropicToken } from "../src/utils/oauth/anthropic.ts"; +import type { AuthEvent, AuthPrompt } from "../src/auth/types.ts"; +import { anthropicOAuth, loginAnthropic, refreshAnthropicToken } from "../src/utils/oauth/anthropic.ts"; function jsonResponse(body: unknown, status: number = 200): Response { return new Response(JSON.stringify(body), { @@ -96,4 +97,38 @@ describe.sequential("Anthropic OAuth", () => { expect(credentials.refresh).toBe("new-refresh-token"); expect(fetchMock).toHaveBeenCalledOnce(); }); + + it("anthropicOAuth.login resolves through the manual_code prompt and aborts it after settling", async () => { + const fetchMock = vi.fn(async (input: unknown): Promise => { + const url = typeof input === "string" ? input : String(input); + if (url.includes("/oauth/token")) { + return jsonResponse({ access_token: "access", refresh_token: "refresh", expires_in: 3600 }); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + const events: AuthEvent[] = []; + const prompts: AuthPrompt[] = []; + let manualSignal: AbortSignal | undefined; + + const credential = await anthropicOAuth.login({ + notify: (event) => events.push(event), + prompt: async (prompt) => { + prompts.push(prompt); + if (prompt.type === "manual_code") { + manualSignal = prompt.signal; + return "the-code"; + } + throw new Error(`Unexpected prompt: ${prompt.type}`); + }, + }); + + expect(credential.type).toBe("oauth"); + expect(credential.access).toBe("access"); + expect(events.some((e) => e.type === "auth_url")).toBe(true); + expect(prompts.some((p) => p.type === "manual_code")).toBe(true); + // the prompt's signal is aborted once login settles, so UIs can dismiss it + expect(manualSignal?.aborted).toBe(true); + }); }); diff --git a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts index bb4b739b..44fa5aeb 100644 --- a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts +++ b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-sse-parsing.test.ts b/packages/ai/test/anthropic-sse-parsing.test.ts index d8daf7f7..e510ec55 100644 --- a/packages/ai/test/anthropic-sse-parsing.test.ts +++ b/packages/ai/test/anthropic-sse-parsing.test.ts @@ -1,8 +1,8 @@ import type Anthropic from "@anthropic-ai/sdk"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, ToolCall } from "../src/types.ts"; function createSseResponse(events: Array<{ event: string; data: string }>): Response { diff --git a/packages/ai/test/anthropic-temperature-compat.test.ts b/packages/ai/test/anthropic-temperature-compat.test.ts index 00161ab8..4b059237 100644 --- a/packages/ai/test/anthropic-temperature-compat.test.ts +++ b/packages/ai/test/anthropic-temperature-compat.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicTemperaturePayload { diff --git a/packages/ai/test/anthropic-thinking-disable.test.ts b/packages/ai/test/anthropic-thinking-disable.test.ts index 13d333e5..9ecfeb4f 100644 --- a/packages/ai/test/anthropic-thinking-disable.test.ts +++ b/packages/ai/test/anthropic-thinking-disable.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-tool-name-normalization.test.ts b/packages/ai/test/anthropic-tool-name-normalization.test.ts index bb454081..b8c45049 100644 --- a/packages/ai/test/anthropic-tool-name-normalization.test.ts +++ b/packages/ai/test/anthropic-tool-name-normalization.test.ts @@ -1,7 +1,6 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; +import { getModel, stream } from "../src/compat.ts"; import type { Context, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/azure-openai-base-url.test.ts b/packages/ai/test/azure-openai-base-url.test.ts index 15b8a528..5908ad38 100644 --- a/packages/ai/test/azure-openai-base-url.test.ts +++ b/packages/ai/test/azure-openai-base-url.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamAzureOpenAIResponses } from "../src/providers/azure-openai-responses.ts"; +import { stream as streamAzureOpenAIResponses } from "../src/api/azure-openai-responses.ts"; +import { getModel } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; interface CapturedAzureClientOptions { @@ -96,6 +96,11 @@ describe("azure-openai-responses base URL normalization", () => { expect(baseURL).toBe("https://marc-quicktests-resource.cognitiveservices.azure.com/openai/v1"); }); + it("normalizes Microsoft Foundry root endpoints to /openai/v1", async () => { + const baseURL = await captureClientBaseUrl("https://marc-quicktests-resource.ai.azure.com"); + expect(baseURL).toBe("https://marc-quicktests-resource.ai.azure.com/openai/v1"); + }); + it("normalizes Azure OpenAI root endpoints to /openai/v1", async () => { const baseURL = await captureClientBaseUrl("https://my-resource.openai.azure.com"); expect(baseURL).toBe("https://my-resource.openai.azure.com/openai/v1"); @@ -111,6 +116,11 @@ describe("azure-openai-responses base URL normalization", () => { expect(baseURL).toBe("https://my-resource.cognitiveservices.azure.com/openai/v1"); }); + it("normalizes /openai/v1/responses to /openai/v1", async () => { + const baseURL = await captureClientBaseUrl("https://my-resource.services.ai.azure.com/openai/v1/responses"); + expect(baseURL).toBe("https://my-resource.services.ai.azure.com/openai/v1"); + }); + it("preserves explicit non-Azure proxy paths", async () => { const baseURL = await captureClientBaseUrl("https://my-proxy.example.com/v1"); expect(baseURL).toBe("https://my-proxy.example.com/v1"); diff --git a/packages/ai/test/bedrock-convert-messages.test.ts b/packages/ai/test/bedrock-convert-messages.test.ts index d74dedae..c43f7978 100644 --- a/packages/ai/test/bedrock-convert-messages.test.ts +++ b/packages/ai/test/bedrock-convert-messages.test.ts @@ -44,8 +44,8 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }; }); -import { getModel } from "../src/models.ts"; -import { streamBedrock } from "../src/providers/amazon-bedrock.ts"; +import { stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, Message } from "../src/types.ts"; const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"); diff --git a/packages/ai/test/bedrock-custom-headers.test.ts b/packages/ai/test/bedrock-custom-headers.test.ts index 1017d089..43ee692c 100644 --- a/packages/ai/test/bedrock-custom-headers.test.ts +++ b/packages/ai/test/bedrock-custom-headers.test.ts @@ -51,9 +51,9 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }; }); -import { getModel } from "../src/models.ts"; -import type { BedrockOptions } from "../src/providers/amazon-bedrock.ts"; -import { streamBedrock, streamSimpleBedrock } from "../src/providers/amazon-bedrock.ts"; +import type { BedrockOptions } from "../src/api/bedrock-converse-stream.ts"; +import { stream as streamBedrock, streamSimple as streamSimpleBedrock } from "../src/api/bedrock-converse-stream.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; const context: Context = { diff --git a/packages/ai/test/bedrock-endpoint-resolution.test.ts b/packages/ai/test/bedrock-endpoint-resolution.test.ts index 18be2476..168cf4d1 100644 --- a/packages/ai/test/bedrock-endpoint-resolution.test.ts +++ b/packages/ai/test/bedrock-endpoint-resolution.test.ts @@ -44,8 +44,8 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }; }); -import { getModel } from "../src/models.ts"; -import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.ts"; +import { type BedrockOptions, stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; const context: Context = { diff --git a/packages/ai/test/bedrock-models.test.ts b/packages/ai/test/bedrock-models.test.ts index 2cfd8fb9..08f95e7a 100644 --- a/packages/ai/test/bedrock-models.test.ts +++ b/packages/ai/test/bedrock-models.test.ts @@ -17,8 +17,7 @@ */ import { describe, expect, it } from "vitest"; -import { getModels } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModels } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/bedrock-thinking-payload.test.ts b/packages/ai/test/bedrock-thinking-payload.test.ts index 8f4e06e7..d2de4913 100644 --- a/packages/ai/test/bedrock-thinking-payload.test.ts +++ b/packages/ai/test/bedrock-thinking-payload.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.ts"; +import { type BedrockOptions, stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index 6e2c1a5b..c80ad19c 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,10 +1,9 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; +import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts"; +import { getModel, stream } from "../src/compat.ts"; import { MODELS } from "../src/models.generated.ts"; -import { getModel } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; -import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; -import { stream } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; class PayloadCaptured extends Error { diff --git a/packages/ai/test/codex-websocket-cached-probe.ts b/packages/ai/test/codex-websocket-cached-probe.ts index 7317035c..d8154ac1 100644 --- a/packages/ai/test/codex-websocket-cached-probe.ts +++ b/packages/ai/test/codex-websocket-cached-probe.ts @@ -10,13 +10,13 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { Type } from "typebox"; import { AuthStorage } from "../../coding-agent/src/core/auth-storage.ts"; -import { getModel } from "../src/models.ts"; import { closeOpenAICodexWebSocketSessions, getOpenAICodexWebSocketDebugStats, resetOpenAICodexWebSocketDebugStats, - streamOpenAICodexResponses, -} from "../src/providers/openai-codex-responses.ts"; + stream as streamOpenAICodexResponses, +} from "../src/api/openai-codex-responses.ts"; +import { getModel } from "../src/compat.ts"; import type { AssistantMessage, Context, Message, Model, Tool, ToolResultMessage, Transport } from "../src/types.ts"; type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; diff --git a/packages/ai/test/compat-env.test.ts b/packages/ai/test/compat-env.test.ts new file mode 100644 index 00000000..59928211 --- /dev/null +++ b/packages/ai/test/compat-env.test.ts @@ -0,0 +1,74 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { complete, registerApiProvider, resetApiProviders } from "../src/compat.ts"; +import type { AssistantMessage, Context, Model } from "../src/types.ts"; +import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; + +const context: Context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] }; + +const model: Model<"openai-responses"> = { + id: "test-model", + name: "Test Model", + api: "openai-responses", + provider: "custom-openai", + baseUrl: "https://example.test/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 4096, +}; + +function message(): AssistantMessage { + return { + role: "assistant", + content: [{ type: "text", text: "ok" }], + 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(), + }; +} + +describe("compat legacy API fallback", () => { + afterEach(() => { + resetApiProviders(); + }); + + it("dispatches unknown providers through the legacy API registry", async () => { + let capturedApiKey: string | undefined; + registerApiProvider({ + api: "openai-responses", + stream: (_model, _context, options) => { + capturedApiKey = options?.apiKey; + const stream = new AssistantMessageEventStream(); + const output = message(); + stream.push({ type: "start", partial: output }); + stream.push({ type: "done", reason: "stop", message: output }); + stream.end(output); + return stream; + }, + streamSimple: (_model, _context, options) => { + capturedApiKey = options?.apiKey; + const stream = new AssistantMessageEventStream(); + const output = message(); + stream.push({ type: "start", partial: output }); + stream.push({ type: "done", reason: "stop", message: output }); + stream.end(output); + return stream; + }, + }); + + await complete(model, context, { apiKey: "request-key" }); + + expect(capturedApiKey).toBe("request-key"); + }); +}); diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 9f021a41..68305949 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -14,8 +14,7 @@ import type { ChildProcess } from "child_process"; import { execSync, spawn } from "child_process"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { getModel, getModels } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel, getModels } from "../src/compat.ts"; import type { AssistantMessage, Context, Model, Usage } from "../src/types.ts"; import { isContextOverflow } from "../src/utils/overflow.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index 23593394..57a43b0a 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -25,8 +25,7 @@ import { writeFileSync } from "fs"; import { Type } from "typebox"; import { beforeAll, describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { completeSimple, getEnvApiKey } from "../src/stream.ts"; +import { completeSimple, getEnvApiKey, getModel } from "../src/compat.ts"; import type { Api, AssistantMessage, Message, Model, Tool, ToolResultMessage } from "../src/types.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index a8453dcc..86c25aa4 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Api, AssistantMessage, Context, Model, StreamOptions, UserMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/faux-provider.test.ts b/packages/ai/test/faux-provider.test.ts index 4f110c54..3d8a190f 100644 --- a/packages/ai/test/faux-provider.test.ts +++ b/packages/ai/test/faux-provider.test.ts @@ -8,7 +8,7 @@ import { registerFauxProvider, stream, Type, -} from "../src/index.ts"; +} from "../src/compat.ts"; import type { AssistantMessageEvent, Context } from "../src/types.ts"; async function collectEvents(streamResult: ReturnType): Promise { diff --git a/packages/ai/test/fireworks-models.test.ts b/packages/ai/test/fireworks-models.test.ts index 8b291b89..a0c9c915 100644 --- a/packages/ai/test/fireworks-models.test.ts +++ b/packages/ai/test/fireworks-models.test.ts @@ -2,9 +2,9 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht import type { AddressInfo } from "node:net"; import { Type } from "typebox"; import { afterEach, describe, expect, it } from "vitest"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; +import { getModel, getModels } from "../src/compat.ts"; import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts"; -import { getModel, getModels } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; import type { Context, Model, Tool } from "../src/types.ts"; const originalFireworksApiKey = process.env.FIREWORKS_API_KEY; @@ -79,7 +79,16 @@ const tool: Tool = { parameters: Type.Object({ value: Type.String() }), }; -function createFireworksModel(compat?: Model<"anthropic-messages">["compat"]): Model<"anthropic-messages"> { +const FIREWORKS_ANTHROPIC_COMPAT = { + sendSessionAffinityHeaders: true, + supportsEagerToolInputStreaming: false, + supportsCacheControlOnTools: false, + supportsLongCacheRetention: false, +} satisfies NonNullable["compat"]>; + +function createFireworksModel( + compat: Model<"anthropic-messages">["compat"] = FIREWORKS_ANTHROPIC_COMPAT, +): Model<"anthropic-messages"> { return { id: "accounts/fireworks/models/kimi-k2p6", name: "Kimi K2.6", diff --git a/packages/ai/test/github-copilot-anthropic.test.ts b/packages/ai/test/github-copilot-anthropic.test.ts index ace2bd8f..74a95418 100644 --- a/packages/ai/test/github-copilot-anthropic.test.ts +++ b/packages/ai/test/github-copilot-anthropic.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; -import { getModel, getSupportedThinkingLevels } from "../src/models.ts"; -import { streamAnthropic } from "../src/providers/anthropic.ts"; +import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts"; +import { getModel } from "../src/compat.ts"; +import { getSupportedThinkingLevels } from "../src/models.ts"; import type { Context } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/github-copilot-oauth.test.ts b/packages/ai/test/github-copilot-oauth.test.ts index c94370da..f5e426e3 100644 --- a/packages/ai/test/github-copilot-oauth.test.ts +++ b/packages/ai/test/github-copilot-oauth.test.ts @@ -1,5 +1,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { loginGitHubCopilot } from "../src/utils/oauth/github-copilot.ts"; +import { getModels } from "../src/compat.ts"; +import { + githubCopilotOAuthProvider, + loginGitHubCopilot, + refreshGitHubCopilotToken, +} from "../src/utils/oauth/github-copilot.ts"; function jsonResponse(body: unknown, status: number = 200): Response { return new Response(JSON.stringify(body), { @@ -29,6 +34,57 @@ describe("GitHub Copilot OAuth device flow", () => { vi.useRealTimers(); }); + it("filters models to the authenticated account picker catalog", async () => { + const fetchMock = vi.fn(async (input: unknown, init?: RequestInit): Promise => { + const url = getUrl(input); + + if (url.includes("/copilot_internal/v2/token")) { + return jsonResponse({ + token: "tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;", + expires_at: 9999999999, + }); + } + + if (url === "https://api.individual.githubcopilot.com/models") { + expect(init?.headers).toMatchObject({ + Authorization: "Bearer tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;", + }); + return jsonResponse({ + data: [ + { + id: "gpt-4.1", + model_picker_enabled: true, + capabilities: { supports: { tool_calls: true } }, + }, + { + id: "claude-opus-4.7", + model_picker_enabled: true, + policy: { state: "disabled" }, + capabilities: { supports: { tool_calls: true } }, + }, + { + id: "gpt-5.4-nano", + model_picker_enabled: false, + capabilities: { supports: { tool_calls: true } }, + }, + ], + }); + } + + throw new Error(`Unexpected fetch URL: ${url}`); + }); + + vi.stubGlobal("fetch", fetchMock); + + const credentials = await refreshGitHubCopilotToken("ghu_refresh_token"); + expect(credentials.availableModelIds).toEqual(["gpt-4.1"]); + + const modifiedModels = githubCopilotOAuthProvider.modifyModels?.(getModels("github-copilot"), credentials) ?? []; + expect(modifiedModels.filter((model) => model.provider === "github-copilot").map((model) => model.id)).toEqual([ + "gpt-4.1", + ]); + }); + it("reports device-code details through onDeviceCode", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-09T00:00:00Z")); @@ -57,6 +113,10 @@ describe("GitHub Copilot OAuth device flow", () => { }); } + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } + if (url.includes("/models/") && url.endsWith("/policy")) { return new Response("", { status: 200 }); } @@ -146,6 +206,10 @@ describe("GitHub Copilot OAuth device flow", () => { }); } + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } + if (url.includes("/models/") && url.endsWith("/policy")) { return new Response("", { status: 200 }); } @@ -231,6 +295,10 @@ describe("GitHub Copilot OAuth device flow", () => { }); } + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } + if (url.includes("/models/") && url.endsWith("/policy")) { return new Response("", { status: 200 }); } diff --git a/packages/ai/test/google-shared-convert-tools.test.ts b/packages/ai/test/google-shared-convert-tools.test.ts index c7b10b41..d91bcfa0 100644 --- a/packages/ai/test/google-shared-convert-tools.test.ts +++ b/packages/ai/test/google-shared-convert-tools.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { convertTools } from "../src/providers/google-shared.ts"; +import { convertTools } from "../src/api/google-shared.ts"; import type { Tool } from "../src/types.ts"; function makeTool(parameters: Record): Tool { diff --git a/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts b/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts index 920c6a4e..406c5a37 100644 --- a/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts +++ b/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { convertMessages } from "../src/providers/google-shared.ts"; +import { convertMessages } from "../src/api/google-shared.ts"; import type { Context, Model } from "../src/types.ts"; function makeGemini3Model( diff --git a/packages/ai/test/google-shared-image-tool-result-routing.test.ts b/packages/ai/test/google-shared-image-tool-result-routing.test.ts index 1430a084..8ba5660c 100644 --- a/packages/ai/test/google-shared-image-tool-result-routing.test.ts +++ b/packages/ai/test/google-shared-image-tool-result-routing.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { convertMessages } from "../src/providers/google-shared.ts"; +import { convertMessages } from "../src/api/google-shared.ts"; import type { Context, Model } from "../src/types.ts"; function makeModel( diff --git a/packages/ai/test/google-thinking-disable.test.ts b/packages/ai/test/google-thinking-disable.test.ts index 30df3638..3f6e36c7 100644 --- a/packages/ai/test/google-thinking-disable.test.ts +++ b/packages/ai/test/google-thinking-disable.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Api, Context, Model, SimpleStreamOptions } from "../src/types.ts"; type SimpleOptionsWithExtras = SimpleStreamOptions & Record; diff --git a/packages/ai/test/google-thinking-signature.test.ts b/packages/ai/test/google-thinking-signature.test.ts index 83b17ccc..853a6a60 100644 --- a/packages/ai/test/google-thinking-signature.test.ts +++ b/packages/ai/test/google-thinking-signature.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { isThinkingPart, retainThoughtSignature } from "../src/providers/google-shared.ts"; +import { isThinkingPart, retainThoughtSignature } from "../src/api/google-shared.ts"; describe("Google thinking detection (thoughtSignature)", () => { it("treats part.thought === true as thinking", () => { diff --git a/packages/ai/test/google-vertex-api-key-resolution.test.ts b/packages/ai/test/google-vertex-api-key-resolution.test.ts index 5f66649c..46f24a77 100644 --- a/packages/ai/test/google-vertex-api-key-resolution.test.ts +++ b/packages/ai/test/google-vertex-api-key-resolution.test.ts @@ -45,8 +45,8 @@ vi.mock("@google/genai", () => { }; }); -import { getModel } from "../src/models.ts"; -import { streamGoogleVertex } from "../src/providers/google-vertex.ts"; +import { stream as streamGoogleVertex } from "../src/api/google-vertex.ts"; +import { getModel } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; const model = getModel("google-vertex", "gemini-3-flash-preview"); diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index a752b7ce..946a443a 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -2,8 +2,8 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import type { Api, Context, Model, Tool, ToolResultMessage } from "../src/index.ts"; -import { complete, getModel } from "../src/index.ts"; +import type { Api, Context, Model, Tool, ToolResultMessage } from "../src/compat.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/images-models.test.ts b/packages/ai/test/images-models.test.ts new file mode 100644 index 00000000..0a2a9e56 --- /dev/null +++ b/packages/ai/test/images-models.test.ts @@ -0,0 +1,207 @@ +import { describe, expect, it } from "vitest"; +import type { AuthContext } from "../src/auth/types.ts"; +import { createImagesModels, createImagesProvider, type ImagesProvider } from "../src/images-models.ts"; +import { builtinImagesModels } from "../src/providers/all.ts"; +import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ImagesOptions } from "../src/types.ts"; + +function fakeAuthContext(env: Record): AuthContext { + return { + env: async (name) => env[name], + fileExists: async () => false, + }; +} + +function testImageModel(provider: string, id: string): ImagesModel { + return { + id, + name: id, + api: "test-images", + provider, + baseUrl: "https://example.test/v1", + input: ["text"], + output: ["image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }; +} + +function okResult(model: ImagesModel): AssistantImages { + return { + api: model.api, + provider: model.provider, + model: model.id, + output: [{ type: "image", data: "aGk=", mimeType: "image/png" }], + stopReason: "stop", + timestamp: Date.now(), + }; +} + +interface GenerateCall { + model: ImagesModel; + options: ImagesOptions | undefined; +} + +function testProvider(input: { + id: string; + models?: ImagesModel[]; + envVar?: string; + calls?: GenerateCall[]; +}): ImagesProvider { + return createImagesProvider({ + id: input.id, + auth: { + apiKey: { + name: "Test key", + resolve: async ({ ctx }) => { + if (!input.envVar) return { auth: {} }; + const key = await ctx.env(input.envVar); + return key ? { auth: { apiKey: key }, source: input.envVar } : undefined; + }, + }, + }, + models: input.models ?? [testImageModel(input.id, "model-a")], + api: { + generateImages: async (model, _context, options) => { + input.calls?.push({ model, options }); + return okResult(model); + }, + }, + }); +} + +const context: ImagesContext = { input: [{ type: "text", text: "a red circle" }] }; + +describe("ImagesModels", () => { + it("registers providers and reads models synchronously", () => { + const models = createImagesModels(); + models.setProvider(testProvider({ id: "p1", models: [testImageModel("p1", "m1"), testImageModel("p1", "m2")] })); + models.setProvider(testProvider({ id: "p2", models: [testImageModel("p2", "m3")] })); + + expect(models.getProviders().map((p) => p.id)).toEqual(["p1", "p2"]); + expect(models.getModels().map((m) => m.id)).toEqual(["m1", "m2", "m3"]); + expect(models.getModels("p1").map((m) => m.id)).toEqual(["m1", "m2"]); + expect(models.getModel("p2", "m3")?.id).toBe("m3"); + expect(models.getModel("p2", "missing")).toBeUndefined(); + + models.deleteProvider("p1"); + expect(models.getProvider("p1")).toBeUndefined(); + }); + + it("resolves auth through the provider and merges it into requests; explicit options win", async () => { + const calls: GenerateCall[] = []; + const models = createImagesModels({ authContext: fakeAuthContext({ TEST_KEY: "env-key" }) }); + models.setProvider(testProvider({ id: "p1", envVar: "TEST_KEY", calls })); + const model = models.getModel("p1", "model-a")!; + + expect((await models.getAuth(model))?.auth.apiKey).toBe("env-key"); + + const result = await models.generateImages(model, context); + expect(result.stopReason).toBe("stop"); + expect(calls[0].options?.apiKey).toBe("env-key"); + + await models.generateImages(model, context, { apiKey: "explicit" }); + expect(calls[1].options?.apiKey).toBe("explicit"); + }); + + it("merges provider-resolved env into image options", async () => { + const calls: GenerateCall[] = []; + const models = createImagesModels(); + models.setProvider( + createImagesProvider({ + id: "p1", + auth: { + apiKey: { + name: "Test key", + resolve: async () => ({ + auth: { apiKey: "provider-key" }, + env: { PROVIDER_ONLY: "provider", SHARED: "provider" }, + }), + }, + }, + models: [testImageModel("p1", "model-a")], + api: { + generateImages: async (model, _context, options) => { + calls.push({ model, options }); + return okResult(model); + }, + }, + }), + ); + const model = models.getModel("p1", "model-a")!; + + await models.generateImages(model, context, { + apiKey: "request-key", + env: { REQUEST_ONLY: "request", SHARED: "request" }, + }); + + expect(calls[0].options?.apiKey).toBe("request-key"); + expect(calls[0].options?.env).toEqual({ + PROVIDER_ONLY: "provider", + REQUEST_ONLY: "request", + SHARED: "request", + }); + }); + + it("returns an error result for unknown providers and unconfigured auth rejections", async () => { + const models = createImagesModels({ authContext: fakeAuthContext({}) }); + const ghost = await models.generateImages(testImageModel("ghost", "m"), context); + expect(ghost.stopReason).toBe("error"); + expect(ghost.errorMessage).toContain("Unknown provider: ghost"); + + // unconfigured (resolve -> undefined) still dispatches; provider decides what to do + const calls: GenerateCall[] = []; + models.setProvider(testProvider({ id: "p1", envVar: "MISSING", calls })); + const model = models.getModel("p1", "model-a")!; + expect(await models.getAuth(model)).toBeUndefined(); + await models.generateImages(model, context); + expect(calls[0].options?.apiKey).toBeUndefined(); + }); + + it("supports dynamic providers via refresh with in-flight dedupe", async () => { + let fetches = 0; + const provider = createImagesProvider({ + id: "dyn", + auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, + models: [], + refreshModels: async () => { + fetches++; + await new Promise((resolve) => setTimeout(resolve, 5)); + return [testImageModel("dyn", "listed")]; + }, + api: { generateImages: async (model) => okResult(model) }, + }); + const models = createImagesModels(); + models.setProvider(provider); + + expect(models.getModels("dyn")).toEqual([]); + await Promise.all([models.refresh("dyn"), models.refresh("dyn")]); + expect(fetches).toBe(1); + expect(models.getModel("dyn", "listed")).toBeDefined(); + + // failures reject with ModelsError for a single provider + models.setProvider( + createImagesProvider({ + id: "flaky", + auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, + models: [], + refreshModels: async () => { + throw new Error("fetch failed"); + }, + api: { generateImages: async (model) => okResult(model) }, + }), + ); + await expect(models.refresh("flaky")).rejects.toMatchObject({ code: "model_source" }); + await expect(models.refresh()).resolves.toBeUndefined(); + }); + + it("builtinImagesModels registers the openrouter provider with its catalog", async () => { + const models = builtinImagesModels({ authContext: fakeAuthContext({ OPENROUTER_API_KEY: "or-key" }) }); + const providers = models.getProviders(); + expect(providers.map((p) => p.id)).toEqual(["openrouter"]); + + const list = models.getModels("openrouter"); + expect(list.length).toBeGreaterThan(0); + expect(list.every((m) => m.api === "openrouter-images")).toBe(true); + + expect((await models.getAuth(list[0]))?.auth.apiKey).toBe("or-key"); + }); +}); diff --git a/packages/ai/test/interleaved-thinking.test.ts b/packages/ai/test/interleaved-thinking.test.ts index 4cf387ce..b4da1283 100644 --- a/packages/ai/test/interleaved-thinking.test.ts +++ b/packages/ai/test/interleaved-thinking.test.ts @@ -1,8 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { completeSimple, getModel } from "../src/compat.ts"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { getModel } from "../src/models.ts"; -import { completeSimple } from "../src/stream.ts"; import type { Api, Context, Model, StopReason, Tool, ToolCall, ToolResultMessage } from "../src/types.ts"; import { StringEnum } from "../src/utils/typebox-helpers.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index e21f0d12..dd516962 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -5,6 +5,8 @@ import { describe, expect, it } from "vitest"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href; +const compatEntryUrl = new URL("../src/compat.ts", import.meta.url).href; +const providersAllUrl = new URL("../src/providers/all.ts", import.meta.url).href; const SDK_SPECIFIERS = [ "@anthropic-ai/sdk", @@ -66,8 +68,25 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual([]); }); - it("loads only the Anthropic SDK when calling the root lazy wrapper", () => { + it("does not load provider SDKs when building all builtin providers", () => { const result = runProbe(` + const all = await import(${JSON.stringify(providersAllUrl)}); + const models = all.builtinModels(); + models.getModels(); + `); + expect(result.loadedSpecifiers).toEqual([]); + }); + + it("does not load provider SDKs when importing the compat entrypoint", () => { + const result = runProbe(` + await import(${JSON.stringify(compatEntryUrl)}); + `); + expect(result.loadedSpecifiers).toEqual([]); + }); + + it("loads only the Anthropic SDK when streaming through the lazy API wrapper", () => { + const result = runProbe(` + const compat = await import(${JSON.stringify(compatEntryUrl)}); const model = { id: "claude-sonnet-4-6", name: "Claude Sonnet 4", @@ -81,7 +100,7 @@ describe("lazy provider module loading", () => { maxTokens: 8192, }; const context = { messages: [{ role: "user", content: "hi" }] }; - await mod.streamSimpleAnthropic(model, context).result(); + await compat.anthropicMessagesApi().streamSimple(model, context).result(); `); expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); @@ -89,9 +108,10 @@ describe("lazy provider module loading", () => { it("loads only the Anthropic SDK when dispatching through streamSimple", () => { const result = runProbe(` - const model = mod.getModel("anthropic", "claude-sonnet-4-6"); + const compat = await import(${JSON.stringify(compatEntryUrl)}); + const model = compat.getModel("anthropic", "claude-sonnet-4-6"); const context = { messages: [{ role: "user", content: "hi" }] }; - await mod.streamSimple(model, context).result(); + await compat.streamSimple(model, context).result(); `); expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); diff --git a/packages/ai/test/mistral-reasoning-mode.test.ts b/packages/ai/test/mistral-reasoning-mode.test.ts index 35a5b96b..bffa292d 100644 --- a/packages/ai/test/mistral-reasoning-mode.test.ts +++ b/packages/ai/test/mistral-reasoning-mode.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface MistralPayload { promptMode?: "reasoning"; reasoningEffort?: "none" | "high"; + promptCacheKey?: string; } function makeContext(): Context { @@ -77,4 +77,21 @@ describe("Mistral reasoning mode selection", () => { expect(payload.reasoningEffort).toBeUndefined(); expect(payload.promptMode).toBeUndefined(); }); + + it("uses the session id as prompt cache key", async () => { + const payload = await capturePayload(getModel("mistral", "mistral-large-latest"), { + sessionId: "session-123", + }); + + expect(payload.promptCacheKey).toBe("session-123"); + }); + + it("omits prompt cache key when cache retention is disabled", async () => { + const payload = await capturePayload(getModel("mistral", "mistral-large-latest"), { + sessionId: "session-123", + cacheRetention: "none", + }); + + expect(payload.promptCacheKey).toBeUndefined(); + }); }); diff --git a/packages/ai/test/mistral-tool-schema.test.ts b/packages/ai/test/mistral-tool-schema.test.ts index c6898fc6..7691775a 100644 --- a/packages/ai/test/mistral-tool-schema.test.ts +++ b/packages/ai/test/mistral-tool-schema.test.ts @@ -1,7 +1,6 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; interface MistralToolPayload { diff --git a/packages/ai/test/models-runtime.test.ts b/packages/ai/test/models-runtime.test.ts new file mode 100644 index 00000000..7f805e26 --- /dev/null +++ b/packages/ai/test/models-runtime.test.ts @@ -0,0 +1,440 @@ +import { describe, expect, it } from "vitest"; +import { InMemoryCredentialStore } from "../src/auth/credential-store.ts"; +import type { ApiKeyAuth, CredentialStore, OAuthAuth, ProviderAuth } from "../src/auth/types.ts"; +import { createModels, hasApi, type Provider } from "../src/models.ts"; +import type { Api, AssistantMessage, Context, Model, SimpleStreamOptions, StreamOptions } from "../src/types.ts"; +import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; + +function testModel(provider: string, id: string): Model { + return { + id, + name: id, + api: "test-api", + provider, + baseUrl: "https://example.test/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 10000, + maxTokens: 1000, + }; +} + +function doneMessage(model: Model, text: string): AssistantMessage { + return { + role: "assistant", + content: [{ type: "text", text }], + 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(), + }; +} + +interface ProviderCall { + model: Model; + options: StreamOptions | undefined; +} + +/** Ambient auth for keyless test providers; reports "configured" with no auth values. */ +const ambientAuth: ApiKeyAuth = { + name: "Ambient", + resolve: async () => ({ auth: {} }), +}; + +function testProvider(input: { + id: string; + models?: Model[]; + auth?: ProviderAuth; + getModels?: () => readonly Model[]; + refreshModels?: () => Promise; + calls?: ProviderCall[]; +}): Provider { + const models = input.models ?? [testModel(input.id, "model-a")]; + const respond = (model: Model, options: StreamOptions | undefined) => { + input.calls?.push({ model, options }); + const stream = new AssistantMessageEventStream(); + const message = doneMessage(model, "ok"); + stream.push({ type: "start", partial: message }); + stream.push({ type: "done", reason: "stop", message }); + stream.end(message); + return stream; + }; + return { + id: input.id, + name: input.id, + auth: input.auth ?? { apiKey: ambientAuth }, + getModels: input.getModels ?? (() => models), + refreshModels: input.refreshModels, + stream: (model, _context, options) => respond(model, options as StreamOptions | undefined), + streamSimple: (model, _context, options) => respond(model, options as SimpleStreamOptions | undefined), + }; +} + +const context: Context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] }; + +function envKeyAuth(key: string | undefined): ApiKeyAuth { + return { + name: "Test API key", + resolve: async ({ credential }) => { + const resolved = credential?.key ?? key; + if (!resolved) return undefined; + return { auth: { apiKey: resolved }, source: credential ? "stored" : "env" }; + }, + }; +} + +function testOAuth(overrides?: Partial): OAuthAuth { + return { + name: "Test OAuth", + login: async () => { + throw new Error("not used"); + }, + refresh: async (credential) => credential, + toAuth: async (credential) => ({ apiKey: credential.access }), + ...overrides, + }; +} + +describe("Models runtime", () => { + it("registers, replaces, and deletes providers", () => { + const models = createModels(); + models.setProvider(testProvider({ id: "p1" })); + models.setProvider(testProvider({ id: "p2" })); + expect(models.getProviders().map((p) => p.id)).toEqual(["p1", "p2"]); + + const replacement = testProvider({ id: "p1" }); + models.setProvider(replacement); + expect(models.getProvider("p1")).toBe(replacement); + expect(models.getProviders()).toHaveLength(2); + + models.deleteProvider("p1"); + expect(models.getProvider("p1")).toBeUndefined(); + + models.clearProviders(); + expect(models.getProviders()).toHaveLength(0); + }); + + it("lists and finds models per provider", async () => { + const models = createModels(); + models.setProvider(testProvider({ id: "p1", models: [testModel("p1", "m1"), testModel("p1", "m2")] })); + models.setProvider(testProvider({ id: "p2", models: [testModel("p2", "m3")] })); + + expect(models.getModels().map((m) => m.id)).toEqual(["m1", "m2", "m3"]); + expect(models.getModels("p1").map((m) => m.id)).toEqual(["m1", "m2"]); + expect(models.getModels("nope").length).toBe(0); + expect(models.getModel("p2", "m3")?.id).toBe("m3"); + expect(models.getModel("p2", "missing")).toBeUndefined(); + + // hasApi() narrows dynamically looked-up models with a runtime check + const found = models.getModel("p2", "m3"); + expect(found && hasApi(found, "openai-completions")).toBe(false); + expect(found && hasApi(found, "test-api")).toBe(true); + if (found && hasApi(found, "test-api")) { + const _typed: Model<"test-api"> = found; + expect(_typed.id).toBe("m3"); + } + }); + + it("swallows provider source failures for both all-provider and single-provider listing", () => { + const models = createModels(); + models.setProvider( + testProvider({ + id: "broken", + getModels: () => { + throw new Error("boom"); + }, + }), + ); + models.setProvider(testProvider({ id: "ok", models: [testModel("ok", "m1")] })); + + expect(models.getModels().map((m) => m.id)).toEqual(["m1"]); + expect(models.getModels("broken")).toEqual([]); + // precise failures come from the provider directly + expect(() => models.getProvider("broken")?.getModels()).toThrow("boom"); + }); + + it("refresh() updates dynamic providers; single-provider refresh failures reject", async () => { + let list = [testModel("dyn", "before")]; + let refreshes = 0; + const models = createModels(); + models.setProvider( + testProvider({ + id: "dyn", + getModels: () => list, + refreshModels: async () => { + refreshes++; + list = [testModel("dyn", "after")]; + }, + }), + ); + models.setProvider(testProvider({ id: "static", models: [testModel("static", "s1")] })); + + expect(models.getModel("dyn", "before")).toBeDefined(); + await models.refresh("dyn"); + expect(refreshes).toBe(1); + expect(models.getModel("dyn", "after")).toBeDefined(); + expect(models.getModel("dyn", "before")).toBeUndefined(); + + // static providers are no-ops; refresh-all is best-effort + await models.refresh("static"); + await models.refresh(); + expect(refreshes).toBe(2); + + // single-provider refresh failures reject with ModelsError + models.setProvider( + testProvider({ + id: "flaky", + refreshModels: async () => { + throw new Error("fetch failed"); + }, + }), + ); + await expect(models.refresh("flaky")).rejects.toMatchObject({ code: "model_source" }); + // refresh-all swallows the same failure + await expect(models.refresh()).resolves.toBeUndefined(); + }); + + it("resolves auth: stored credential owns the provider, ambient only when nothing stored", async () => { + const credentials = new InMemoryCredentialStore(); + const models = createModels({ credentials }); + models.setProvider(testProvider({ id: "p1", auth: { apiKey: envKeyAuth("env-key"), oauth: testOAuth() } })); + const model = testModel("p1", "model-a"); + + // nothing stored: ambient env resolves + expect((await models.getAuth(model))?.auth.apiKey).toBe("env-key"); + + // stored oauth credential (persisted via the single write path): beats ambient env + await credentials.modify("p1", async () => ({ + type: "oauth", + access: "oauth-token", + refresh: "r", + expires: Date.now() + 100000, + })); + const resolution = await models.getAuth(model); + expect(resolution?.auth.apiKey).toBe("oauth-token"); + expect(resolution?.source).toBe("OAuth"); + + // stored api-key credential resolves through apiKey auth, beats env + await credentials.modify("p1", async () => ({ type: "api_key", key: "stored-key" })); + const apiKeyResolution = await models.getAuth(model); + expect(apiKeyResolution?.auth.apiKey).toBe("stored-key"); + expect(apiKeyResolution?.source).toBe("stored"); + }); + + it("a stored credential without a matching handler blocks ambient fallback", async () => { + const credentials = new InMemoryCredentialStore(); + const models = createModels({ credentials }); + // provider has only apiKey auth, but an oauth credential is stored (stale config) + models.setProvider(testProvider({ id: "p1", auth: { apiKey: envKeyAuth("env-key") } })); + await credentials.modify("p1", async () => ({ type: "oauth", access: "a", refresh: "r", expires: 0 })); + + expect(await models.getAuth(testModel("p1", "model-a"))).toBeUndefined(); + }); + + it("refreshes expired oauth credentials and persists the rotated credential", async () => { + const credentials = new InMemoryCredentialStore(); + const oauth = testOAuth({ + refresh: async (credential) => ({ ...credential, access: "new-token", expires: Date.now() + 60_000 }), + }); + const models = createModels({ credentials }); + models.setProvider(testProvider({ id: "p1", auth: { oauth } })); + await credentials.modify("p1", async () => ({ + type: "oauth", + access: "old-token", + refresh: "r", + expires: 0, + })); + + const resolution = await models.getAuth(testModel("p1", "model-a")); + expect(resolution?.auth.apiKey).toBe("new-token"); + expect(((await credentials.read("p1")) as { access: string }).access).toBe("new-token"); + }); + + it("rejects with code oauth when refresh fails, preserving the stored credential", async () => { + const credentials = new InMemoryCredentialStore(); + const oauth = testOAuth({ + refresh: async () => { + throw new Error("invalid_grant"); + }, + }); + const models = createModels({ credentials }); + models.setProvider(testProvider({ id: "p1", auth: { oauth } })); + await credentials.modify("p1", async () => ({ type: "oauth", access: "old", refresh: "r", expires: 0 })); + + await expect(models.getAuth(testModel("p1", "model-a"))).rejects.toMatchObject({ code: "oauth" }); + // credential preserved for retry / re-login + expect(((await credentials.read("p1")) as { access: string }).access).toBe("old"); + }); + + it("serializes concurrent OAuth refreshes through store.modify (no double refresh)", async () => { + const credentials = new InMemoryCredentialStore(); + await credentials.modify("p1", async () => ({ type: "oauth", access: "old", refresh: "r1", expires: 0 })); + + let refreshes = 0; + const oauth = testOAuth({ + refresh: async () => { + refreshes++; + await new Promise((resolve) => setTimeout(resolve, 10)); + return { type: "oauth", access: `new-${refreshes}`, refresh: "r2", expires: Date.now() + 60_000 }; + }, + }); + const models = createModels({ credentials }); + models.setProvider(testProvider({ id: "p1", auth: { oauth } })); + const model = testModel("p1", "model-a"); + + const [a, b] = await Promise.all([models.getAuth(model), models.getAuth(model)]); + expect(refreshes).toBe(1); + expect(a?.auth.apiKey).toBe("new-1"); + expect(b?.auth.apiKey).toBe("new-1"); + }); + + it("valid oauth tokens resolve without touching modify", async () => { + let modifies = 0; + const base = new InMemoryCredentialStore(); + const credentials: CredentialStore = { + read: (pid) => base.read(pid), + modify: (pid, fn) => { + modifies++; + return base.modify(pid, fn); + }, + delete: (pid) => base.delete(pid), + }; + await base.modify("p1", async () => ({ + type: "oauth", + access: "valid", + refresh: "r", + expires: Date.now() + 60_000, + })); + const models = createModels({ credentials }); + models.setProvider(testProvider({ id: "p1", auth: { oauth: testOAuth() } })); + + expect((await models.getAuth(testModel("p1", "model-a")))?.auth.apiKey).toBe("valid"); + expect(modifies).toBe(0); + }); + + it("wraps credential store failures in ModelsError", async () => { + // read failure + const readFailing: CredentialStore = { + read: async () => { + throw new Error("disk on fire"); + }, + modify: async () => undefined, + delete: async () => {}, + }; + const models = createModels({ credentials: readFailing }); + models.setProvider(testProvider({ id: "p1", auth: { apiKey: envKeyAuth("env-key") } })); + await expect(models.getAuth(testModel("p1", "model-a"))).rejects.toMatchObject({ code: "auth" }); + + // modify failure during refresh + const modifyFailing: CredentialStore = { + read: async () => ({ type: "oauth", access: "old", refresh: "r", expires: 0 }), + modify: async () => { + throw new Error("disk on fire"); + }, + delete: async () => {}, + }; + const oauthModels = createModels({ credentials: modifyFailing }); + oauthModels.setProvider(testProvider({ id: "p1", auth: { oauth: testOAuth() } })); + await expect(oauthModels.getAuth(testModel("p1", "model-a"))).rejects.toMatchObject({ code: "auth" }); + }); + + it("wraps api-key auth failures in ModelsError", async () => { + const failing: ApiKeyAuth = { + name: "Failing", + resolve: async () => { + throw new Error("nope"); + }, + }; + const models = createModels(); + models.setProvider(testProvider({ id: "p1", auth: { apiKey: failing } })); + await expect(models.getAuth(testModel("p1", "model-a"))).rejects.toMatchObject({ code: "auth" }); + }); + + it("uses explicit request api key and env during provider auth resolution", async () => { + const calls: ProviderCall[] = []; + const apiKey: ApiKeyAuth = { + name: "Scoped", + resolve: async ({ credential, ctx }) => { + const account = credential?.env?.ACCOUNT_ID ?? (await ctx.env("ACCOUNT_ID")); + if (!credential?.key || !account) return undefined; + return { + auth: { apiKey: credential.key, baseUrl: `https://example.test/${account}` }, + env: { ACCOUNT_ID: account }, + }; + }, + }; + const models = createModels(); + models.setProvider(testProvider({ id: "p1", auth: { apiKey }, calls })); + const model = testModel("p1", "model-a"); + + await models.completeSimple(model, context, { apiKey: "explicit-key", env: { ACCOUNT_ID: "acct" } }); + + expect(calls[0].model.baseUrl).toBe("https://example.test/acct"); + expect(calls[0].options?.apiKey).toBe("explicit-key"); + expect(calls[0].options?.env).toEqual({ ACCOUNT_ID: "acct" }); + }); + + it("merges resolved auth into stream options; explicit options win per field", async () => { + const calls: ProviderCall[] = []; + const apiKey: ApiKeyAuth = { + name: "Test", + resolve: async () => ({ + auth: { + apiKey: "resolved-key", + headers: { "x-a": "auth", "x-b": "auth" }, + baseUrl: "https://auth.test/v1", + }, + }), + }; + const models = createModels(); + models.setProvider(testProvider({ id: "p1", auth: { apiKey }, calls })); + const model = testModel("p1", "model-a"); + + const result = await models.completeSimple(model, context, { + apiKey: "explicit-key", + headers: { "x-b": "explicit" }, + }); + expect(result.stopReason).toBe("stop"); + expect(calls).toHaveLength(1); + expect(calls[0].options?.apiKey).toBe("explicit-key"); + expect(calls[0].options?.headers).toEqual({ "x-a": "auth", "x-b": "explicit" }); + expect(calls[0].model.baseUrl).toBe("https://auth.test/v1"); + + // without explicit options, resolved auth applies + const result2 = await models.completeSimple(model, context); + expect(result2.stopReason).toBe("stop"); + expect(calls[1].options?.apiKey).toBe("resolved-key"); + }); + + it("produces an error stream for unknown providers instead of throwing", async () => { + const models = createModels(); + const result = await models.completeSimple(testModel("ghost", "model-a"), context); + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toContain("Unknown provider: ghost"); + }); + + it("streams through the provider", async () => { + const models = createModels(); + models.setProvider(testProvider({ id: "p1" })); + const model = testModel("p1", "model-a"); + + const events: string[] = []; + const stream = models.streamSimple(model, context); + for await (const event of stream) { + events.push(event.type); + } + expect(events).toEqual(["start", "done"]); + const message = await stream.result(); + expect(message.stopReason).toBe("stop"); + }); +}); diff --git a/packages/ai/test/oauth-auth.test.ts b/packages/ai/test/oauth-auth.test.ts new file mode 100644 index 00000000..43be6f72 --- /dev/null +++ b/packages/ai/test/oauth-auth.test.ts @@ -0,0 +1,129 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { InMemoryCredentialStore } from "../src/auth/credential-store.ts"; +import { createModels } from "../src/models.ts"; +import { anthropicProvider } from "../src/providers/anthropic.ts"; +import { githubCopilotProvider } from "../src/providers/github-copilot.ts"; +import { anthropicOAuth } from "../src/utils/oauth/anthropic.ts"; +import { githubCopilotOAuth } from "../src/utils/oauth/github-copilot.ts"; +import { openaiCodexOAuth } from "../src/utils/oauth/openai-codex.ts"; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } }); +} + +describe.sequential("OAuthAuth adapters", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("anthropic toAuth derives the api key from the access token", async () => { + const auth = await anthropicOAuth.toAuth({ type: "oauth", access: "token", refresh: "r", expires: 0 }); + expect(auth).toEqual({ apiKey: "token" }); + }); + + it("openai-codex toAuth derives the api key from the access token", async () => { + const auth = await openaiCodexOAuth.toAuth({ type: "oauth", access: "token", refresh: "r", expires: 0 }); + expect(auth).toEqual({ apiKey: "token" }); + }); + + it("github-copilot toAuth derives baseUrl from the token proxy endpoint", async () => { + const access = "tid=abc;exp=123;proxy-ep=proxy.enterprise.example;rest"; + const auth = await githubCopilotOAuth.toAuth({ type: "oauth", access, refresh: "r", expires: 0 }); + expect(auth).toEqual({ apiKey: access, baseUrl: "https://api.enterprise.example" }); + }); + + it("github-copilot toAuth falls back to the enterprise domain, then the individual endpoint", async () => { + const enterprise = await githubCopilotOAuth.toAuth({ + type: "oauth", + access: "no-proxy-ep", + refresh: "r", + expires: 0, + enterpriseUrl: "https://company.ghe.com", + }); + expect(enterprise.baseUrl).toBe("https://copilot-api.company.ghe.com"); + + const individual = await githubCopilotOAuth.toAuth({ + type: "oauth", + access: "no-proxy-ep", + refresh: "r", + expires: 0, + }); + expect(individual.baseUrl).toBe("https://api.individual.githubcopilot.com"); + }); + + it("anthropic refresh exchanges the refresh token and returns a typed credential", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + jsonResponse({ access_token: "new-access", refresh_token: "new-refresh", expires_in: 3600 }), + ), + ); + + const refreshed = await anthropicOAuth.refresh({ type: "oauth", access: "old", refresh: "old-r", expires: 0 }); + expect(refreshed.type).toBe("oauth"); + expect(refreshed.access).toBe("new-access"); + expect(refreshed.refresh).toBe("new-refresh"); + expect(refreshed.expires).toBeGreaterThan(Date.now()); + }); + + it("github-copilot refresh preserves the enterprise domain", async () => { + const fetchedUrls: string[] = []; + const fetchMock = vi.fn(async (input: unknown) => { + const url = typeof input === "string" ? input : String(input); + fetchedUrls.push(url); + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } + return jsonResponse({ token: "new-token", expires_at: 9999999999 }); + }); + vi.stubGlobal("fetch", fetchMock); + + const refreshed = await githubCopilotOAuth.refresh({ + type: "oauth", + access: "old", + refresh: "gh-token", + expires: 0, + enterpriseUrl: "company.ghe.com", + }); + expect(refreshed.access).toBe("new-token"); + expect(refreshed.enterpriseUrl).toBe("company.ghe.com"); + expect(fetchedUrls[0]).toContain("api.company.ghe.com"); + }); +}); + +describe("OAuth through Models.getAuth (lazy load chain)", () => { + it("resolves stored anthropic oauth credentials via the lazy flow import", async () => { + const credentials = new InMemoryCredentialStore(); + await credentials.modify("anthropic", async () => ({ + type: "oauth", + access: "oauth-access-token", + refresh: "r", + expires: Date.now() + 60_000, + })); + const models = createModels({ credentials }); + models.setProvider(anthropicProvider()); + + const model = models.getModels("anthropic")[0]; + const result = await models.getAuth(model); + expect(result?.auth.apiKey).toBe("oauth-access-token"); + expect(result?.source).toBe("OAuth"); + }); + + it("resolves stored github-copilot oauth credentials including per-credential baseUrl", async () => { + const access = "tid=abc;exp=123;proxy-ep=proxy.business.githubcopilot.com;rest"; + const credentials = new InMemoryCredentialStore(); + await credentials.modify("github-copilot", async () => ({ + type: "oauth", + access, + refresh: "r", + expires: Date.now() + 60_000, + })); + const models = createModels({ credentials }); + models.setProvider(githubCopilotProvider()); + + const model = models.getModels("github-copilot")[0]; + const result = await models.getAuth(model); + expect(result?.auth.apiKey).toBe(access); + expect(result?.auth.baseUrl).toBe("https://api.business.githubcopilot.com"); + }); +}); diff --git a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts index 0abb46f6..19bf857b 100644 --- a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index fe4f3ea6..b68d5b73 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -5,9 +5,9 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { getOpenAICodexWebSocketDebugStats, resetOpenAICodexWebSocketDebugStats, - streamOpenAICodexResponses, - streamSimpleOpenAICodexResponses, -} from "../src/providers/openai-codex-responses.ts"; + stream as streamOpenAICodexResponses, + streamSimple as streamSimpleOpenAICodexResponses, +} from "../src/api/openai-codex-responses.ts"; import type { Context, Model } from "../src/types.ts"; const originalAgentDir = process.env.PI_CODING_AGENT_DIR; @@ -1195,6 +1195,68 @@ describe("openai-codex streaming", () => { }); }); + it("reconnects once when the websocket connection limit is reached before output starts", async () => { + const token = mockToken(); + let connections = 0; + + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + + class MockWebSocket extends EventTarget { + private readonly limitReached = connections++ === 0; + + constructor() { + super(); + queueMicrotask(() => this.dispatchEvent(new Event("open"))); + } + + send(): void { + const event = this.limitReached + ? { type: "error", error: { code: "websocket_connection_limit_reached" } } + : { + type: "response.completed", + response: { + id: "resp_1", + status: "completed", + usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8 }, + }, + }; + queueMicrotask(() => { + this.dispatchEvent(Object.assign(new Event("message"), { data: JSON.stringify(event) })); + }); + } + + close(): void {} + } + + vi.stubGlobal("WebSocket", MockWebSocket); + + const model: Model<"openai-codex-responses"> = { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + }; + + const result = await streamOpenAICodexResponses( + model, + { systemPrompt: "", messages: [] }, + { + apiKey: token, + }, + ).result(); + + expect(result.stopReason).toBe("stop"); + expect(connections).toBe(2); + expect(fetchMock).not.toHaveBeenCalled(); + }); + it("falls back to SSE when a websocket is idle before the first event", async () => { vi.useFakeTimers(); const token = mockToken(); diff --git a/packages/ai/test/openai-completions-cache-control-format.test.ts b/packages/ai/test/openai-completions-cache-control-format.test.ts index 7d8ee9c4..d87a95eb 100644 --- a/packages/ai/test/openai-completions-cache-control-format.test.ts +++ b/packages/ai/test/openai-completions-cache-control-format.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; +import { getModel } from "../src/compat.ts"; import type { Model } from "../src/types.ts"; interface CacheControl { diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index be233670..e83f63ae 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; // Empty tools arrays must NOT be serialized as `tools: []` — some OpenAI-compatible // backends (e.g. DashScope / Aliyun Qwen via compatible-mode) reject the request with @@ -128,6 +127,7 @@ describe("openai-completions empty tools handling", () => { }); it("uses conservative OpenAI-compatible fields for Cloudflare AI Gateway /compat models", async () => { + process.env.CLOUDFLARE_API_KEY = "cf-token"; process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; const model = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!; @@ -138,7 +138,7 @@ describe("openai-completions empty tools handling", () => { systemPrompt: "You are helpful.", messages: [{ role: "user", content: "hi", timestamp: Date.now() }], }, - { apiKey: "test", maxTokens: 1234, reasoning: "high" }, + { maxTokens: 1234, reasoning: "high" }, ).result(); const params = mockState.lastParams as { @@ -160,35 +160,25 @@ describe("openai-completions empty tools handling", () => { }; expect(clientOptions.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/compat"); expect(clientOptions.defaultHeaders?.Authorization).toBeNull(); - expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer test"); + expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer cf-token"); }); - it("uses provider env before process.env for Cloudflare AI Gateway base URL", async () => { - process.env.CLOUDFLARE_ACCOUNT_ID = "process-account"; - process.env.CLOUDFLARE_GATEWAY_ID = "process-gateway"; + it("resolves Cloudflare AI Gateway base URL through provider auth", async () => { + process.env.CLOUDFLARE_API_KEY = "cf-token"; + process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; + process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; const model = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!; - await streamSimple( - model, - { - messages: [{ role: "user", content: "hi", timestamp: Date.now() }], - }, - { - apiKey: "test", - env: { - CLOUDFLARE_ACCOUNT_ID: "provider-account", - CLOUDFLARE_GATEWAY_ID: "provider-gateway", - }, - }, - ).result(); + await streamSimple(model, { + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }).result(); const clientOptions = mockState.lastClientOptions as { baseURL?: string }; - expect(clientOptions.baseURL).toBe( - "https://gateway.ai.cloudflare.com/v1/provider-account/provider-gateway/compat", - ); + expect(clientOptions.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/compat"); }); it("preserves inline upstream Authorization for Cloudflare AI Gateway BYOK requests", async () => { + process.env.CLOUDFLARE_API_KEY = "cf-token"; process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; const model = getModel("cloudflare-ai-gateway", "gpt-5.1")!; @@ -198,7 +188,7 @@ describe("openai-completions empty tools handling", () => { { messages: [{ role: "user", content: "hi", timestamp: Date.now() }], }, - { apiKey: "cf-token", headers: { Authorization: "Bearer upstream-token" } }, + { headers: { Authorization: "Bearer upstream-token" } }, ).result(); const clientOptions = mockState.lastClientOptions as { defaultHeaders?: Record }; @@ -207,6 +197,7 @@ describe("openai-completions empty tools handling", () => { }); it("sends session affinity headers for Workers AI through Cloudflare AI Gateway", async () => { + process.env.CLOUDFLARE_API_KEY = "cf-token"; process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; const workersModel = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!; @@ -216,7 +207,7 @@ describe("openai-completions empty tools handling", () => { { messages: [{ role: "user", content: "hi", timestamp: Date.now() }], }, - { apiKey: "test", sessionId: "session-1" }, + { sessionId: "session-1" }, ).result(); const clientOptions = mockState.lastClientOptions as { defaultHeaders?: Record }; diff --git a/packages/ai/test/openai-completions-prompt-cache.test.ts b/packages/ai/test/openai-completions-prompt-cache.test.ts index 75098fc1..5e905712 100644 --- a/packages/ai/test/openai-completions-prompt-cache.test.ts +++ b/packages/ai/test/openai-completions-prompt-cache.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; +import { getModel } from "../src/compat.ts"; import type { Model } from "../src/types.ts"; interface FakeOpenAIClientOptions { diff --git a/packages/ai/test/openai-completions-reasoning-details.test.ts b/packages/ai/test/openai-completions-reasoning-details.test.ts new file mode 100644 index 00000000..88d42874 --- /dev/null +++ b/packages/ai/test/openai-completions-reasoning-details.test.ts @@ -0,0 +1,118 @@ +import { Type } from "typebox"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; +import type { AssistantMessage, Model, Tool } from "../src/types.ts"; + +const mockState = vi.hoisted(() => ({ + chunkSets: [] as unknown[][], + payloads: [] as unknown[], +})); + +vi.mock("openai", () => { + class FakeOpenAI { + chat = { + completions: { + create: (payload: unknown) => { + mockState.payloads.push(payload); + const chunks = mockState.chunkSets.shift() ?? []; + const stream = { + async *[Symbol.asyncIterator]() { + for (const chunk of chunks) { + yield chunk; + } + }, + }; + const result = Promise.resolve(stream) as Promise & { + withResponse: () => Promise<{ data: typeof stream; response: { status: number; headers: Headers } }>; + }; + result.withResponse = async () => ({ + data: stream, + response: { status: 200, headers: new Headers() }, + }); + return result; + }, + }, + }; + } + return { default: FakeOpenAI }; +}); + +const reasoningDetail = { type: "reasoning.encrypted", id: "call_1", data: "encrypted-signature" }; +const readTool: Tool = { + name: "read", + description: "Read a file", + parameters: Type.Object({ path: Type.String() }), +}; + +function model(): Model<"openai-completions"> { + return { + id: "google/gemini-test", + name: "Gemini Test", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 100_000, + maxTokens: 4096, + }; +} + +function chunk(delta: Record, finishReason: string | null = null): unknown { + return { + id: "chatcmpl-test", + model: "google/gemini-test", + choices: [{ index: 0, delta, finish_reason: finishReason }], + }; +} + +function toolCallChunk(): unknown { + return chunk({ + tool_calls: [ + { + index: 0, + id: "call_1", + type: "function", + function: { name: "read", arguments: '{"path":"README.md"}' }, + }, + ], + }); +} + +async function runOpenAICompletionsStream(messages: AssistantMessage[] = []): Promise { + return await streamOpenAICompletions(model(), { messages, tools: [readTool] }, { apiKey: "test" }).result(); +} + +function getAssistantPayload(payload: unknown): { reasoning_details?: unknown } | undefined { + const messages = (payload as { messages?: Array<{ role?: string; reasoning_details?: unknown }> }).messages ?? []; + return messages.find((message) => message.role === "assistant"); +} + +describe("openai-completions reasoning_details streaming", () => { + beforeEach(() => { + mockState.chunkSets = []; + mockState.payloads = []; + }); + + it("preserves reasoning_details that arrive before their matching tool call", async () => { + mockState.chunkSets = [ + [chunk({ reasoning_details: [reasoningDetail] }), toolCallChunk(), chunk({}, "tool_calls")], + [chunk({ content: "ok" }), chunk({}, "stop")], + ]; + + const assistantMessage = await runOpenAICompletionsStream(); + const toolCall = assistantMessage.content.find((block) => block.type === "toolCall"); + expect(toolCall).toMatchObject({ + type: "toolCall", + id: "call_1", + name: "read", + arguments: { path: "README.md" }, + thoughtSignature: JSON.stringify(reasoningDetail), + }); + + await runOpenAICompletionsStream([assistantMessage]); + + expect(getAssistantPayload(mockState.payloads[1])?.reasoning_details).toEqual([reasoningDetail]); + }); +}); diff --git a/packages/ai/test/openai-completions-response-model.test.ts b/packages/ai/test/openai-completions-response-model.test.ts index d8e4d2f9..5139bd54 100644 --- a/packages/ai/test/openai-completions-response-model.test.ts +++ b/packages/ai/test/openai-completions-response-model.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { complete } from "../src/stream.ts"; +import { complete } from "../src/compat.ts"; import type { Model } from "../src/types.ts"; // Router/virtual ids (e.g. OpenRouter `auto`) keep `model` pinned to the diff --git a/packages/ai/test/openai-completions-retry.test.ts b/packages/ai/test/openai-completions-retry.test.ts index cf631dd1..f67dbbbc 100644 --- a/packages/ai/test/openai-completions-retry.test.ts +++ b/packages/ai/test/openai-completions-retry.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; import type { Context, Model } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/openai-completions-thinking-as-text.test.ts b/packages/ai/test/openai-completions-thinking-as-text.test.ts index 1c49aad3..d1cbe14a 100644 --- a/packages/ai/test/openai-completions-thinking-as-text.test.ts +++ b/packages/ai/test/openai-completions-thinking-as-text.test.ts @@ -2,7 +2,7 @@ import { once } from "node:events"; import http from "node:http"; import type { AddressInfo } from "node:net"; import { afterEach, describe, expect, it } from "vitest"; -import { convertMessages, streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { convertMessages, stream as streamOpenAICompletions } from "../src/api/openai-completions.ts"; import type { AssistantMessage, AssistantMessageEvent, @@ -34,6 +34,7 @@ const compat = { thinkingFormat: "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: true, cacheControlFormat: undefined, diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 8ca86025..927e34b3 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1,9 +1,8 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.ts"; -import { convertMessages } from "../src/providers/openai-completions.ts"; -import { stream, streamSimple } from "../src/stream.ts"; -import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.ts"; +import { convertMessages } from "../src/api/openai-completions.ts"; +import { getModel, stream, streamSimple } from "../src/compat.ts"; +import type { AssistantMessage, Model, SimpleStreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ lastParams: undefined as unknown, @@ -64,6 +63,46 @@ vi.mock("openai", () => { return { default: FakeOpenAI }; }); +const localOpenAICompletionsModel = { + api: "openai-completions", + provider: "local-vllm", + baseUrl: "http://localhost:8000/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192, +} satisfies Omit, "id" | "name" | "compat">; + +type CapturedParams = { + chat_template_kwargs?: Record; + thinking?: unknown; + reasoning_effort?: string; +}; + +async function captureSimpleParams( + model: Model<"openai-completions">, + reasoning?: SimpleStreamOptions["reasoning"], +): Promise { + let payload: unknown; + + await streamSimple( + model, + { + messages: [{ role: "user", content: "Hi", timestamp: Date.now() }], + }, + { + apiKey: "test", + reasoning, + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + return (payload ?? mockState.lastParams) as CapturedParams; +} + describe("openai-completions tool_choice", () => { beforeEach(() => { mockState.lastParams = undefined; @@ -970,6 +1009,8 @@ describe("openai-completions tool_choice", () => { }); it("stores OpenRouter Kimi K2.6 reasoning replay compat in built-in metadata", () => { + // `:free` variant delisted from the OpenRouter API; the generator override + // matches any `moonshotai/kimi-k2.6*` variant that is listed. const model = getModel("openrouter", "moonshotai/kimi-k2.6")!; expect(model.compat?.supportsDeveloperRole).toBe(false); expect(model.compat?.requiresReasoningContentOnAssistantMessages).toBe(true); @@ -1142,6 +1183,7 @@ describe("openai-completions tool_choice", () => { thinkingFormat: "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: true, sendSessionAffinityHeaders: false, @@ -1449,6 +1491,77 @@ describe("openai-completions tool_choice", () => { expect(params.reasoning_effort).toBeUndefined(); }); + it("uses configurable chat template boolean thinking kwargs", async () => { + const model = { + ...localOpenAICompletionsModel, + id: "deepseek-ai/DeepSeek-V3.1", + name: "DeepSeek V3.1 via vLLM", + compat: { + thinkingFormat: "chat-template", + supportsReasoningEffort: false, + chatTemplateKwargs: { thinking: { $var: "thinking.enabled" } }, + }, + } satisfies Model<"openai-completions">; + + for (const testCase of [ + { reasoning: "high" as const, expected: true }, + { reasoning: undefined, expected: false }, + ]) { + const params = await captureSimpleParams(model, testCase.reasoning); + + expect(params.chat_template_kwargs).toEqual({ thinking: testCase.expected }); + expect(params.thinking).toBeUndefined(); + expect(params.reasoning_effort).toBeUndefined(); + } + }); + + it("uses qwen chat template thinking kwargs", async () => { + const model = { + ...localOpenAICompletionsModel, + id: "Qwen/Qwen3-Coder", + name: "Qwen3 Coder via vLLM", + compat: { + thinkingFormat: "qwen-chat-template", + supportsReasoningEffort: false, + }, + } satisfies Model<"openai-completions">; + + for (const testCase of [ + { reasoning: "high" as const, expected: true }, + { reasoning: undefined, expected: false }, + ]) { + const params = await captureSimpleParams(model, testCase.reasoning); + + expect(params.chat_template_kwargs).toEqual({ + enable_thinking: testCase.expected, + preserve_thinking: true, + }); + expect(params.reasoning_effort).toBeUndefined(); + } + }); + + it("uses configurable chat template effort kwargs with static kwargs", async () => { + const model = { + ...localOpenAICompletionsModel, + id: "unsloth/gpt-oss-120b-GGUF", + name: "GPT OSS via vLLM", + thinkingLevelMap: { xhigh: "max" }, + compat: { + thinkingFormat: "chat-template", + supportsReasoningEffort: false, + chatTemplateKwargs: { + preserve_thinking: true, + reasoning_effort: { $var: "thinking.effort", omitWhenOff: true }, + }, + }, + } satisfies Model<"openai-completions">; + + const params = await captureSimpleParams(model, "xhigh"); + + expect(params.chat_template_kwargs).toEqual({ preserve_thinking: true, reasoning_effort: "max" }); + expect(params.reasoning_effort).toBeUndefined(); + }); + it("uses Ant Ling compatibility metadata", async () => { const model = getModel("ant-ling", "Ring-2.6-1T")!; let payload: unknown; diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index 3510f396..c8500792 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { convertMessages } from "../src/providers/openai-completions.ts"; +import { convertMessages } from "../src/api/openai-completions.ts"; +import { getModel } from "../src/compat.ts"; import type { AssistantMessage, Context, @@ -32,6 +32,7 @@ const compat: Required = { thinkingFormat: "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: true, cacheControlFormat: "anthropic", diff --git a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts index d173694f..a2f1197e 100644 --- a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Context } from "../src/types.ts"; describe.skipIf(!process.env.OPENAI_API_KEY)("openai responses cache affinity e2e", () => { diff --git a/packages/ai/test/openai-responses-copilot-provider.test.ts b/packages/ai/test/openai-responses-copilot-provider.test.ts index 04236fed..57c319f2 100644 --- a/packages/ai/test/openai-responses-copilot-provider.test.ts +++ b/packages/ai/test/openai-responses-copilot-provider.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.ts"; -import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; +import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts"; +import { getModel } from "../src/compat.ts"; import type { Model } from "../src/types.ts"; type CapturedHeaders = Headers | string[][] | Record | undefined; diff --git a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts index 10f06377..b8231658 100644 --- a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts +++ b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { convertResponsesMessages } from "../src/providers/openai-responses-shared.ts"; +import { convertResponsesMessages } from "../src/api/openai-responses-shared.ts"; +import { getModel } from "../src/compat.ts"; import type { AssistantMessage, Context, ToolResultMessage, Usage } from "../src/types.ts"; import { shortHash } from "../src/utils/hash.ts"; diff --git a/packages/ai/test/openai-responses-message-id.test.ts b/packages/ai/test/openai-responses-message-id.test.ts index f675cc8e..cb2fd0c0 100644 --- a/packages/ai/test/openai-responses-message-id.test.ts +++ b/packages/ai/test/openai-responses-message-id.test.ts @@ -1,7 +1,7 @@ import type { ResponseOutputMessage } from "openai/resources/responses/responses.js"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { convertResponsesMessages } from "../src/providers/openai-responses-shared.ts"; +import { convertResponsesMessages } from "../src/api/openai-responses-shared.ts"; +import { getModel } from "../src/compat.ts"; import type { AssistantMessage, Context, Usage } from "../src/types.ts"; const usage: Usage = { diff --git a/packages/ai/test/openai-responses-partial-json-cleanup.test.ts b/packages/ai/test/openai-responses-partial-json-cleanup.test.ts index 76b16ad3..e4f4de4d 100644 --- a/packages/ai/test/openai-responses-partial-json-cleanup.test.ts +++ b/packages/ai/test/openai-responses-partial-json-cleanup.test.ts @@ -1,6 +1,6 @@ import type { ResponseStreamEvent } from "openai/resources/responses/responses.js"; import { describe, expect, it, vi } from "vitest"; -import { processResponsesStream } from "../src/providers/openai-responses-shared.ts"; +import { processResponsesStream } from "../src/api/openai-responses-shared.ts"; import type { AssistantMessage, AssistantMessageEvent, Model } from "../src/types.ts"; import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; @@ -57,6 +57,11 @@ async function* createFunctionCallEvents(argumentsJson: string): AsyncIterable { diff --git a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts index aa6f2e23..85753c03 100644 --- a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts +++ b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts @@ -1,7 +1,6 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete, getEnvApiKey } from "../src/stream.ts"; +import { complete, getEnvApiKey, getModel } from "../src/compat.ts"; import type { AssistantMessage, Context, Message, Tool, ToolCall } from "../src/types.ts"; const testToolSchema = Type.Object({ diff --git a/packages/ai/test/openai-responses-terminal-event.test.ts b/packages/ai/test/openai-responses-terminal-event.test.ts new file mode 100644 index 00000000..c37896a3 --- /dev/null +++ b/packages/ai/test/openai-responses-terminal-event.test.ts @@ -0,0 +1,233 @@ +import type { ResponseStreamEvent } from "openai/resources/responses/responses.js"; +import { describe, expect, it, vi } from "vitest"; +import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts"; +import { processResponsesStream } from "../src/api/openai-responses-shared.ts"; +import type { AssistantMessage, AssistantMessageEvent, Context, Model } from "../src/types.ts"; +import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; + +vi.mock("openai", () => { + async function* createMockResponsesStream(): AsyncIterable { + yield { + type: "response.created", + sequence_number: 0, + response: { id: "resp_wrapper_early_eof" }, + } as ResponseStreamEvent; + yield { + type: "response.output_item.added", + sequence_number: 1, + output_index: 0, + item: { type: "reasoning", id: "rs_wrapper_early_eof", summary: [] }, + } as ResponseStreamEvent; + yield { + type: "response.reasoning_text.delta", + sequence_number: 2, + output_index: 0, + content_index: 0, + item_id: "rs_wrapper_early_eof", + delta: "partial reasoning before the wrapper stream ends", + } as ResponseStreamEvent; + } + + class FakeOpenAI { + responses = { + create: () => { + const responseStream = createMockResponsesStream(); + const promise = Promise.resolve(responseStream) as Promise> & { + withResponse: () => Promise<{ + data: AsyncIterable; + response: { status: number; headers: Headers }; + }>; + }; + promise.withResponse = async () => ({ + data: responseStream, + response: { status: 200, headers: new Headers() }, + }); + return promise; + }, + }; + } + + return { default: FakeOpenAI }; +}); + +function createModel(): Model<"openai-responses"> { + return { + id: "gpt-5-mini", + name: "GPT-5 Mini", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + }; +} + +function createOutput(model: Model<"openai-responses">): 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(), + }; +} + +async function* createEarlyEofEvents(): AsyncIterable { + yield { + type: "response.created", + sequence_number: 0, + response: { id: "resp_early_eof" }, + } as ResponseStreamEvent; + yield { + type: "response.output_item.added", + sequence_number: 1, + output_index: 0, + item: { type: "reasoning", id: "rs_early_eof", summary: [] }, + } as ResponseStreamEvent; + yield { + type: "response.reasoning_text.delta", + sequence_number: 2, + output_index: 0, + content_index: 0, + item_id: "rs_early_eof", + delta: "partial reasoning before the stream ends", + } as ResponseStreamEvent; +} + +async function* createCompletedEvents(): AsyncIterable { + yield { + type: "response.completed", + sequence_number: 0, + response: { + id: "resp_completed", + status: "completed", + usage: { + input_tokens: 20, + output_tokens: 7, + total_tokens: 27, + input_tokens_details: { cached_tokens: 2 }, + }, + }, + } as ResponseStreamEvent; +} + +async function* createIncompleteEvents(): AsyncIterable { + yield { + type: "response.incomplete", + sequence_number: 0, + response: { + id: "resp_incomplete", + status: "incomplete", + usage: { + input_tokens: 30, + output_tokens: 12, + total_tokens: 42, + input_tokens_details: { cached_tokens: 5 }, + }, + }, + } as ResponseStreamEvent; +} + +async function* createFailedEvents(): AsyncIterable { + yield { + type: "response.failed", + sequence_number: 0, + response: { + id: "resp_failed", + status: "failed", + error: { code: "server_error", message: "boom" }, + }, + } as ResponseStreamEvent; +} + +describe("OpenAI Responses terminal event handling", () => { + it("rejects streams that end before a terminal response event", async () => { + const model = createModel(); + const output = createOutput(model); + const stream = new AssistantMessageEventStream(); + + await expect(processResponsesStream(createEarlyEofEvents(), output, stream, model)).rejects.toThrow( + "OpenAI Responses stream ended before a terminal response event", + ); + }); + + it("emits an error final result when the wrapper stream ends before a terminal response event", async () => { + const model = createModel(); + const context: Context = { + systemPrompt: "", + messages: [{ role: "user", content: [{ type: "text", text: "hi" }], timestamp: 0 }], + tools: [], + }; + const stream = streamOpenAIResponses(model, context, { apiKey: "test" }); + const events: AssistantMessageEvent[] = []; + + for await (const event of stream) { + events.push(event); + } + + const result = await stream.result(); + const lastEvent = events.at(-1); + expect(lastEvent?.type).toBe("error"); + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toBe("OpenAI Responses stream ended before a terminal response event"); + }); + + it("finalizes completed terminal events as stop", async () => { + const model = createModel(); + const output = createOutput(model); + const stream = new AssistantMessageEventStream(); + + await processResponsesStream(createCompletedEvents(), output, stream, model); + + expect(output.responseId).toBe("resp_completed"); + expect(output.stopReason).toBe("stop"); + expect(output.usage).toMatchObject({ + input: 18, + output: 7, + cacheRead: 2, + cacheWrite: 0, + totalTokens: 27, + }); + }); + + it("finalizes incomplete terminal events as length stops", async () => { + const model = createModel(); + const output = createOutput(model); + const stream = new AssistantMessageEventStream(); + + await processResponsesStream(createIncompleteEvents(), output, stream, model); + + expect(output.responseId).toBe("resp_incomplete"); + expect(output.stopReason).toBe("length"); + expect(output.usage).toMatchObject({ + input: 25, + output: 12, + cacheRead: 5, + cacheWrite: 0, + totalTokens: 42, + }); + }); + + it("rejects failed terminal events with the provider error", async () => { + const model = createModel(); + const output = createOutput(model); + const stream = new AssistantMessageEventStream(); + + await expect(processResponsesStream(createFailedEvents(), output, stream, model)).rejects.toThrow( + "server_error: boom", + ); + }); +}); diff --git a/packages/ai/test/openai-responses-tool-result-images.test.ts b/packages/ai/test/openai-responses-tool-result-images.test.ts index c6131d91..da33ab86 100644 --- a/packages/ai/test/openai-responses-tool-result-images.test.ts +++ b/packages/ai/test/openai-responses-tool-result-images.test.ts @@ -4,8 +4,8 @@ import { fileURLToPath } from "node:url"; import type { ResponseFunctionCallOutputItemList } from "openai/resources/responses/responses.js"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import type { Api, Context, Model, StreamOptions, Tool, ToolResultMessage } from "../src/index.ts"; -import { complete, getModel } from "../src/index.ts"; +import type { Api, Context, Model, StreamOptions, Tool, ToolResultMessage } from "../src/compat.ts"; +import { complete, getModel } from "../src/compat.ts"; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/openrouter-cache-write-repro.test.ts b/packages/ai/test/openrouter-cache-write-repro.test.ts index 4bdeb286..2292ec91 100644 --- a/packages/ai/test/openrouter-cache-write-repro.test.ts +++ b/packages/ai/test/openrouter-cache-write-repro.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { completeSimple } from "../src/stream.ts"; +import { completeSimple, getModel } from "../src/compat.ts"; function createLongSystemPrompt(): string { const nonce = `${Date.now()}-${Math.random()}`; diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts new file mode 100644 index 00000000..0f6e25bd --- /dev/null +++ b/packages/ai/test/providers.test.ts @@ -0,0 +1,313 @@ +import { describe, expect, it } from "vitest"; +import { envApiKeyAuth } from "../src/auth/helpers.ts"; +import type { AuthContext } from "../src/auth/types.ts"; +import { createModels, createProvider } from "../src/models.ts"; +import { builtinModels, builtinProviders } from "../src/providers/all.ts"; +import { amazonBedrockProvider } from "../src/providers/amazon-bedrock.ts"; +import { anthropicProvider } from "../src/providers/anthropic.ts"; +import { cloudflareAIGatewayProvider } from "../src/providers/cloudflare-ai-gateway.ts"; +import { cloudflareWorkersAIProvider } from "../src/providers/cloudflare-workers-ai.ts"; +import { fauxAssistantMessage, fauxProvider } from "../src/providers/faux.ts"; +import { googleVertexProvider } from "../src/providers/google-vertex.ts"; +import type { Api, Context, Model, ProviderStreams } from "../src/types.ts"; +import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; + +function fakeAuthContext(env: Record, files: string[] = []): AuthContext { + return { + env: async (name) => env[name], + fileExists: async (path) => files.includes(path), + }; +} + +const context: Context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] }; + +describe("builtin providers", () => { + it("builtinModels registers every builtin provider with models", async () => { + const models = builtinModels(); + const providers = models.getProviders(); + expect(providers.length).toBe(builtinProviders().length); + expect(providers.map((p) => p.id)).toContain("anthropic"); + + const anthropic = models.getModel("anthropic", "claude-haiku-4-5"); + expect(anthropic?.api).toBe("anthropic-messages"); + + const all = models.getModels(); + expect(all.length).toBeGreaterThan(500); + + // every provider lists at least one model and owns its models + for (const provider of providers) { + const list = models.getModels(provider.id); + expect(list.length).toBeGreaterThan(0); + expect(list.every((m) => m.provider === provider.id)).toBe(true); + } + }); + + it("resolves anthropic auth from env with OAuth token precedence", async () => { + const models = createModels({ + authContext: fakeAuthContext({ ANTHROPIC_API_KEY: "key", ANTHROPIC_OAUTH_TOKEN: "oauth-token" }), + }); + models.setProvider(anthropicProvider()); + const model = models.getModel("anthropic", "claude-haiku-4-5")!; + + const result = await models.getAuth(model); + expect(result?.auth.apiKey).toBe("oauth-token"); + expect(result?.source).toBe("ANTHROPIC_OAUTH_TOKEN"); + }); + + it("reports bedrock as configured from ambient AWS credentials without an api key", async () => { + const models = createModels({ authContext: fakeAuthContext({ AWS_PROFILE: "dev" }) }); + models.setProvider(amazonBedrockProvider()); + const model = models.getModels("amazon-bedrock")[0]; + + const result = await models.getAuth(model); + expect(result?.auth).toEqual({}); + expect(result?.source).toBe("AWS_PROFILE"); + + const unconfigured = createModels({ authContext: fakeAuthContext({}) }); + unconfigured.setProvider(amazonBedrockProvider()); + expect(await unconfigured.getAuth(model)).toBeUndefined(); + }); + + it("requires Cloudflare Workers AI account config and returns scoped env", async () => { + const missingAccount = createModels({ authContext: fakeAuthContext({ CLOUDFLARE_API_KEY: "cf-key" }) }); + missingAccount.setProvider(cloudflareWorkersAIProvider()); + const model = missingAccount.getModels("cloudflare-workers-ai")[0]; + expect(await missingAccount.getAuth(model)).toBeUndefined(); + + const configured = createModels({ + authContext: fakeAuthContext({ CLOUDFLARE_API_KEY: "cf-key", CLOUDFLARE_ACCOUNT_ID: "account-id" }), + }); + configured.setProvider(cloudflareWorkersAIProvider()); + const result = await configured.getAuth(model); + expect(result?.auth).toEqual({ + apiKey: "cf-key", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/account-id/ai/v1", + }); + expect(result?.env).toEqual({ CLOUDFLARE_ACCOUNT_ID: "account-id" }); + }); + + it("requires Cloudflare AI Gateway account and gateway config and returns scoped env headers", async () => { + const missingGateway = createModels({ + authContext: fakeAuthContext({ CLOUDFLARE_API_KEY: "cf-key", CLOUDFLARE_ACCOUNT_ID: "account-id" }), + }); + missingGateway.setProvider(cloudflareAIGatewayProvider()); + const model = missingGateway.getModels("cloudflare-ai-gateway")[0]; + expect(await missingGateway.getAuth(model)).toBeUndefined(); + + const configured = createModels({ + authContext: fakeAuthContext({ + CLOUDFLARE_API_KEY: "cf-key", + CLOUDFLARE_ACCOUNT_ID: "account-id", + CLOUDFLARE_GATEWAY_ID: "gateway-id", + }), + }); + configured.setProvider(cloudflareAIGatewayProvider()); + const result = await configured.getAuth(model); + expect(result?.auth).toEqual({ + headers: { + "cf-aig-authorization": "Bearer cf-key", + Authorization: null, + "x-api-key": null, + }, + baseUrl: "https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/anthropic", + }); + expect(result?.env).toEqual({ + CLOUDFLARE_ACCOUNT_ID: "account-id", + CLOUDFLARE_GATEWAY_ID: "gateway-id", + }); + }); + + it("resolves vertex via ADC file plus project and location", async () => { + const adc = "~/.config/gcloud/application_default_credentials.json"; + const configured = createModels({ + authContext: fakeAuthContext({ GOOGLE_CLOUD_PROJECT: "proj", GOOGLE_CLOUD_LOCATION: "us-central1" }, [adc]), + }); + configured.setProvider(googleVertexProvider()); + const model = configured.getModels("google-vertex")[0]; + + const result = await configured.getAuth(model); + expect(result?.auth).toEqual({}); + expect(result?.source).toContain("application default"); + + // ADC without project/location is not configured + const partial = createModels({ authContext: fakeAuthContext({ GOOGLE_CLOUD_PROJECT: "proj" }, [adc]) }); + partial.setProvider(googleVertexProvider()); + expect(await partial.getAuth(model)).toBeUndefined(); + + // explicit key wins over ADC + const keyed = createModels({ authContext: fakeAuthContext({ GOOGLE_CLOUD_API_KEY: "vertex-key" }) }); + keyed.setProvider(googleVertexProvider()); + expect((await keyed.getAuth(model))?.auth.apiKey).toBe("vertex-key"); + }); +}); + +describe("envApiKeyAuth", () => { + it("prefers the stored credential key and falls back through env vars in order", async () => { + const auth = envApiKeyAuth("Test key", ["FIRST_KEY", "SECOND_KEY"]); + const model = { provider: "p1" } as Model; + + const stored = await auth.resolve({ + model, + ctx: fakeAuthContext({ FIRST_KEY: "env" }), + credential: { type: "api_key", key: "stored" }, + }); + expect(stored?.auth.apiKey).toBe("stored"); + expect(stored?.source).toBe("stored credential"); + + const second = await auth.resolve({ model, ctx: fakeAuthContext({ SECOND_KEY: "second" }) }); + expect(second?.auth.apiKey).toBe("second"); + expect(second?.source).toBe("SECOND_KEY"); + + expect(await auth.resolve({ model, ctx: fakeAuthContext({}) })).toBeUndefined(); + }); + + it("login prompts for a secret and returns an api-key credential", async () => { + const auth = envApiKeyAuth("Test key", ["TEST_KEY"]); + const credential = await auth.login?.({ + prompt: async (prompt) => { + expect(prompt.type).toBe("secret"); + return "entered-key"; + }, + notify: () => {}, + }); + expect(credential).toEqual({ type: "api_key", key: "entered-key" }); + }); +}); + +describe("createProvider", () => { + function recordingStreams(label: string, calls: string[]): ProviderStreams { + const respond = (model: Model) => { + calls.push(`${label}:${model.id}`); + const stream = new AssistantMessageEventStream(); + const message = fauxAssistantMessage("ok"); + stream.push({ type: "start", partial: message }); + stream.push({ type: "done", reason: "stop", message }); + stream.end(message); + return stream; + }; + return { stream: respond, streamSimple: respond }; + } + + function testModel(api: string, id: string): Model { + return { + id, + name: id, + api, + provider: "mixed", + baseUrl: "https://example.test/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 10000, + maxTokens: 1000, + }; + } + + it("dispatches on model.api for mixed-API providers", async () => { + const calls: string[] = []; + const provider = createProvider({ + id: "mixed", + auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, + models: [testModel("api-a", "model-a"), testModel("api-b", "model-b")], + api: { "api-a": recordingStreams("a", calls), "api-b": recordingStreams("b", calls) }, + }); + const models = createModels(); + models.setProvider(provider); + + await models.completeSimple(testModel("api-a", "model-a"), context); + await models.completeSimple(testModel("api-b", "model-b"), context); + expect(calls).toEqual(["a:model-a", "b:model-b"]); + }); + + it("merges provider-resolved env into stream options", async () => { + let capturedEnv: Record | undefined; + let capturedApiKey: string | undefined; + const envModel = { ...testModel("api-a", "model-a"), provider: "env-provider" }; + const provider = createProvider({ + id: "env-provider", + auth: { + apiKey: { + name: "Test", + resolve: async () => ({ + auth: { apiKey: "provider-key" }, + env: { PROVIDER_ONLY: "provider", SHARED: "provider" }, + }), + }, + }, + models: [envModel], + api: { + stream: (model, _context, options) => { + capturedEnv = options?.env; + capturedApiKey = options?.apiKey; + return recordingStreams("a", []).stream(model, _context, options); + }, + streamSimple: (model, _context, options) => { + capturedEnv = options?.env; + capturedApiKey = options?.apiKey; + return recordingStreams("a", []).streamSimple(model, _context, options); + }, + }, + }); + const models = createModels(); + models.setProvider(provider); + + await models.completeSimple(envModel, context, { + apiKey: "request-key", + env: { REQUEST_ONLY: "request", SHARED: "request" }, + }); + + expect(capturedApiKey).toBe("request-key"); + expect(capturedEnv).toEqual({ PROVIDER_ONLY: "provider", REQUEST_ONLY: "request", SHARED: "request" }); + }); + + it("produces a stream error for a model whose api has no implementation", async () => { + const provider = createProvider({ + id: "mixed", + auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, + models: [testModel("api-a", "model-a")], + api: { "api-a": recordingStreams("a", []) }, + }); + const result = await provider.streamSimple(testModel("api-ghost", "model-x"), context).result(); + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toContain("no API implementation"); + }); + + it("supports dynamic providers: empty until refreshed, in-flight refreshes deduped", async () => { + let fetches = 0; + const provider = createProvider({ + id: "dynamic", + auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } }, + models: [], + refreshModels: async () => { + fetches++; + await new Promise((resolve) => setTimeout(resolve, 5)); + return [testModel("api-a", "listed")]; + }, + api: recordingStreams("a", []), + }); + + expect(provider.getModels()).toEqual([]); + await Promise.all([provider.refreshModels?.(), provider.refreshModels?.()]); + expect(fetches).toBe(1); + expect(provider.getModels().map((m) => m.id)).toEqual(["listed"]); + + // a later refresh fetches again + await provider.refreshModels?.(); + expect(fetches).toBe(2); + }); +}); + +describe("fauxProvider", () => { + it("streams queued responses through a Models collection", async () => { + const faux = fauxProvider(); + const models = createModels(); + models.setProvider(faux.provider); + faux.setResponses([fauxAssistantMessage("hello from faux")]); + + const model = models.getModels(faux.provider.id)[0]; + const result = await models.completeSimple(model, context); + expect(result.stopReason).toBe("stop"); + expect(result.content).toEqual([{ type: "text", text: "hello from faux" }]); + expect(faux.state.callCount).toBe(1); + }); +}); diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index d250f5f4..cd566057 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/retry.test.ts b/packages/ai/test/retry.test.ts new file mode 100644 index 00000000..17273c70 --- /dev/null +++ b/packages/ai/test/retry.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { fauxAssistantMessage } from "../src/providers/faux.ts"; +import { isRetryableAssistantError } from "../src/utils/retry.ts"; + +const openAIExplicitRetryMessage = + "An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID req_******** in your message."; +const bedrockExplicitRetryMessage = + '{"message":"The system encountered an unexpected error during processing. Try your request again."}'; + +describe("provider retry classification", () => { + it("matches explicit provider retry guidance", () => { + expect( + isRetryableAssistantError( + fauxAssistantMessage("", { stopReason: "error", errorMessage: openAIExplicitRetryMessage }), + ), + ).toBe(true); + expect( + isRetryableAssistantError( + fauxAssistantMessage("", { stopReason: "error", errorMessage: bedrockExplicitRetryMessage }), + ), + ).toBe(true); + }); + + it("keeps provider limit errors non-retryable", () => { + expect( + isRetryableAssistantError( + fauxAssistantMessage("", { stopReason: "error", errorMessage: "429 quota exceeded" }), + ), + ).toBe(false); + }); + + it("classifies assistant error messages", () => { + expect( + isRetryableAssistantError(fauxAssistantMessage("", { stopReason: "error", errorMessage: "overloaded_error" })), + ).toBe(true); + expect(isRetryableAssistantError(fauxAssistantMessage("not an error"))).toBe(false); + }); +}); diff --git a/packages/ai/test/scratch.ts b/packages/ai/test/scratch.ts new file mode 100644 index 00000000..c2d83649 --- /dev/null +++ b/packages/ai/test/scratch.ts @@ -0,0 +1,57 @@ +// Scratch script showing real-world use of the new Models API. +// Run from packages/ai: node test/scratch.ts +// Requires ANTHROPIC_API_KEY. + +import { createModels } from "../src/models.ts"; +import { anthropicProvider } from "../src/providers/anthropic.ts"; +import type { Context } from "../src/types.ts"; + +// --------------------------------------------------------------------------- +// 1. Build a Models runtime and register a built-in provider factory. +// (Apps wanting everything use `builtinModels()` from providers/all.) +// --------------------------------------------------------------------------- + +const models = createModels(); +models.setProvider(anthropicProvider()); + +// --------------------------------------------------------------------------- +// 2. Look up a model and check auth. +// --------------------------------------------------------------------------- + +const model = models.getModel("anthropic", "claude-haiku-4-5"); +if (!model) throw new Error("model not found"); + +const auth = await models.getAuth(model); +console.log(`model: ${model.provider}/${model.id}`); +console.log(`auth: ${auth ? `configured via ${auth.source}` : "not configured"}\n`); +if (!auth) process.exit(1); + +const context: Context = { + systemPrompt: "You are terse.", + messages: [{ role: "user", content: "Say exactly: ok", timestamp: Date.now() }], +}; + +// --------------------------------------------------------------------------- +// 3. Simple completion (request-level auth resolution happens inside). +// --------------------------------------------------------------------------- + +const message = await models.completeSimple(model, context); +console.log(`completeSimple -> [${message.stopReason}]`, message.content); + +// --------------------------------------------------------------------------- +// 4. Streaming with deltas. +// --------------------------------------------------------------------------- + +context.messages.push(message, { + role: "user", + content: "Now count from 1 to 5, one number per line.", + timestamp: Date.now(), +}); + +process.stdout.write("streamSimple -> "); +const stream = models.streamSimple(model, context); +for await (const event of stream) { + if (event.type === "text_delta") process.stdout.write(event.delta.replaceAll("\n", " ")); +} +const final = await stream.result(); +console.log(`[${final.stopReason}] cost: $${final.usage.cost.total.toFixed(6)}`); diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 9a70b6c2..fbc95f50 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -4,8 +4,7 @@ import { dirname, join } from "path"; import { Type } from "typebox"; import { fileURLToPath } from "url"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete, stream } from "../src/stream.ts"; +import { complete, getModel, stream } from "../src/compat.ts"; import type { Api, Context, ImageContent, Model, StreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/supports-xhigh.test.ts b/packages/ai/test/supports-xhigh.test.ts index 9f5363cb..257758dc 100644 --- a/packages/ai/test/supports-xhigh.test.ts +++ b/packages/ai/test/supports-xhigh.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel, getSupportedThinkingLevels } from "../src/models.ts"; +import { getModel, getSupportedThinkingLevels } from "../src/compat.ts"; describe("getSupportedThinkingLevels", () => { it("includes xhigh for Anthropic Opus 4.6 on anthropic-messages API", () => { diff --git a/packages/ai/test/together-models.test.ts b/packages/ai/test/together-models.test.ts index cb5ea943..0d766d65 100644 --- a/packages/ai/test/together-models.test.ts +++ b/packages/ai/test/together-models.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from "vitest"; +import { getModel } from "../src/compat.ts"; import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts"; -import { getModel } from "../src/models.ts"; const originalTogetherApiKey = process.env.TOGETHER_API_KEY; diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index 676f5a29..e99c7134 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel, getModels } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; +import { getModel, getModels, stream } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/tool-call-id-normalization.test.ts b/packages/ai/test/tool-call-id-normalization.test.ts index 0672181b..fd59d9e6 100644 --- a/packages/ai/test/tool-call-id-normalization.test.ts +++ b/packages/ai/test/tool-call-id-normalization.test.ts @@ -12,8 +12,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { completeSimple, getEnvApiKey } from "../src/stream.ts"; +import { completeSimple, getEnvApiKey, getModel } from "../src/compat.ts"; import type { AssistantMessage, Message, Tool, ToolResultMessage } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 11b832f3..198c48c5 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -1,7 +1,6 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions, Tool } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 972913a7..d07fc5ad 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -13,8 +13,7 @@ */ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions, Usage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts b/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts index 4d858835..24f218df 100644 --- a/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts +++ b/packages/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { transformMessages } from "../src/providers/transform-messages.ts"; +import { transformMessages } from "../src/api/transform-messages.ts"; import type { AssistantMessage, Message, Model, ToolCall } from "../src/types.ts"; // Normalize function matching what anthropic.ts uses diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 9cdddefa..f4ea7450 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -1,7 +1,6 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; +import { complete, getModel } from "../src/compat.ts"; import type { Api, Context, Model, StreamOptions, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/xhigh.test.ts b/packages/ai/test/xhigh.test.ts index 3c279863..f1722b9a 100644 --- a/packages/ai/test/xhigh.test.ts +++ b/packages/ai/test/xhigh.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; +import { getModel, stream } from "../src/compat.ts"; import type { Context, Model } from "../src/types.ts"; function makeContext(): Context { diff --git a/packages/ai/test/xiaomi-models.test.ts b/packages/ai/test/xiaomi-models.test.ts index 6fb1f4b8..277b8495 100644 --- a/packages/ai/test/xiaomi-models.test.ts +++ b/packages/ai/test/xiaomi-models.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getModel, getModels } from "../src/models.ts"; +import { getModel, getModels } from "../src/compat.ts"; describe("Xiaomi MiMo models", () => { it("keeps mimo-v2-flash on the API billing provider", () => { diff --git a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts index ab2fec30..74d27089 100644 --- a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts +++ b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { completeSimple, getEnvApiKey, streamSimple } from "../src/stream.ts"; +import { completeSimple, getEnvApiKey, streamSimple } from "../src/compat.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; const provider = "xiaomi-token-plan-ams"; diff --git a/packages/ai/test/zen.test.ts b/packages/ai/test/zen.test.ts index 8ca80014..0f731b46 100644 --- a/packages/ai/test/zen.test.ts +++ b/packages/ai/test/zen.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/compat.ts"; import { MODELS } from "../src/models.generated.ts"; -import { complete } from "../src/stream.ts"; import type { Model } from "../src/types.ts"; describe.skipIf(!process.env.OPENCODE_API_KEY)("OpenCode Models Smoke Test", () => { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a2fd43e8..3dd5d2e1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,12 +2,145 @@ ## [Unreleased] -### Added +### Fixed -- Exported `CONFIG_DIR_NAME` from the coding-agent public API so extensions can resolve project config paths without hardcoding `.pi` ([#5869](https://github.com/earendil-works/pi/pulls/5869) by [@xl0](https://github.com/xl0)) +- Fixed auto-retry for provider stream 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 inherited pi-ai `ApiKeyCredential` to use the `auth.json`-compatible discriminator `type: "api_key"` and provider-scoped `env` values instead of `type: "api-key"` and metadata. +- Renamed the inherited agent-core public harness shell execution options type from `ExecutionEnvExecOptions` to `ShellExecOptions`. + +### Fixed + +- Fixed inherited Anthropic-compatible custom models to use explicit compatibility metadata instead of provider-name heuristics for session-affinity headers and unsupported tool-field omissions. +- Fixed inherited 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 inherited temporary legacy per-API stream aliases such as `streamSimpleOpenAICompletions` on the pi-ai compat entrypoint ([#6016](https://github.com/earendil-works/pi/issues/6016), [#6017](https://github.com/earendil-works/pi/issues/6017)). +- Restored inherited 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 inherited Amazon Bedrock scoped `AWS_PROFILE` endpoint resolution for built-in inference profile endpoints. +- Fixed inherited Fireworks Anthropic-compatible requests to apply session-affinity and unsupported tool-field defaults for custom Fireworks models. +- Fixed inherited Together MiniMax M2.7 metadata to avoid unsupported Together reasoning toggles. + +## [0.80.0] - 2026-06-23 + +### Changed + +- Added `Ctrl+J` as a default newline keybinding alongside `Shift+Enter`. +- Renamed the displayed `zai` provider label to ZAI Coding Plan (Global) for clarity ([#5965](https://github.com/earendil-works/pi/issues/5965)). +- pi-ai's old global API (`stream`/`complete`/`completeSimple`, `getModel`/`getModels`/`getProviders`, `registerApiProvider`, `getEnvApiKey`, ...) moved off the `@earendil-works/pi-ai` root entrypoint to `@earendil-works/pi-ai/compat`. Extensions are not affected at runtime: the extension loader resolves the pi-ai root to the compat entrypoint (a strict superset), so existing extensions keep working unchanged. Extension sources that typecheck against pi-ai's published types should switch those imports to `@earendil-works/pi-ai/compat` (or migrate to the new `createModels()`/provider-factory API). The compat entrypoint and the loader alias will be removed in a future release with a migration guide. + +### Fixed + +- Fixed session names to normalize newline characters before storing or displaying labels ([#5999](https://github.com/earendil-works/pi/pull/5999) by [@haoqixu](https://github.com/haoqixu)). +- Fixed the session selector to order threaded session trees by the latest activity anywhere in each subtree ([#5784](https://github.com/earendil-works/pi/pull/5784) by [@Perlence](https://github.com/Perlence)). +- Fixed extension-related crash and startup-failure reporting to suggest restarting with `pi -ne`. +- Fixed inherited OpenAI Responses streams to fail before missing terminal events and fixed context usage and 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)). +- Fixed inherited 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 inherited Amazon Bedrock endpoint resolution to honor scoped `AWS_PROFILE` values. +- Fixed inherited Cloudflare providers to require account/gateway configuration and route built-in compat calls through provider auth. +- Fixed provider-scoped auth environment values to reach inherited `Models`/`ImagesModels` API calls and compat API-key injection. +- Fixed inherited OpenCode Go GLM-5.2 metadata to expose `xhigh` reasoning and send the provider's max reasoning effort ([#5967](https://github.com/earendil-works/pi/issues/5967)). +- Fixed `pi --resume` to load user package themes and resolve automatic light/dark theme settings. +- Fixed `models.json` custom providers so stored credentials can satisfy auth without a redundant provider-level `apiKey` ([#5953](https://github.com/earendil-works/pi/issues/5953)). + +### Removed + +- Removed inherited selective-provider `@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` entrypoints; use the root packages with explicit `Models` provider factories instead. + +## [0.79.10] - 2026-06-22 + +### New Features + +- **Extension compaction event context** - Extension `session_before_compact` and `session_compact` events now include `reason` and `willRetry`, so extensions can distinguish manual `/compact`, threshold auto-compaction, and overflow retry flows. See [session_before_compact / session_compact](docs/extensions.md#session_before_compact--session_compact) and [Custom Summarization via Extensions](docs/compaction.md#custom-summarization-via-extensions). +- **Safer update flow** - `pi update` installs the exact checked Pi version, and update notices show the changelog URL, making upgrades more predictable. See [Install and Manage](docs/packages.md#install-and-manage). + +### Added + +- Added `reason` and `willRetry` metadata to extension `session_before_compact` and `session_compact` events so extensions can distinguish manual, threshold, and overflow compaction flows ([#5962](https://github.com/earendil-works/pi/pull/5962) by [@PizzaMarinara](https://github.com/PizzaMarinara)). + +### Fixed + +- Fixed the `find` tool to respect nested git repository boundaries when parent `.gitignore` rules ignore the nested repo ([#5960](https://github.com/earendil-works/pi/issues/5960)). +- Fixed the usage docs slash command table to include `/trust` and `/import` ([#5959](https://github.com/earendil-works/pi/issues/5959)). +- Fixed inherited OpenAI-compatible streaming to preserve encrypted `reasoning_details` that arrive before matching tool call deltas ([#5114](https://github.com/earendil-works/pi/issues/5114)). +- Fixed broken TUI documentation links to the plan-mode extension example ([#5957](https://github.com/earendil-works/pi/issues/5957)). +- Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). +- Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). +- Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. +- Fixed update notifications to display the actual changelog URL as the hyperlink text. + +## [0.79.9] - 2026-06-20 + +### New Features + +- **Chat-template thinking compatibility** - OpenAI-compatible custom providers can map Pi thinking levels into `chat_template_kwargs`, enabling vLLM/Hugging Face chat-template models such as DeepSeek to use provider-native thinking controls. See [Custom Provider API Types](docs/custom-provider.md#api-types) and [OpenAI Compatibility](docs/models.md#openai-compatibility). +- **GLM-5.2 provider improvements** - GLM-5.2 now has corrected Fireworks OpenAI-compatible routing and OpenRouter `xhigh` thinking support, improving `/model` behavior and high-effort reasoning for GLM-5.2 users. See [Model Options](docs/usage.md#model-options). + +### Added + +- Added inherited 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 inherited 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 same-directory session switches to reuse imported extension modules while preserving fresh extension instances and lifecycle events ([#5905](https://github.com/earendil-works/pi/issues/5905)). +- Fixed deep session branches taking quadratic time to build context or branch paths ([#5909](https://github.com/earendil-works/pi/issues/5909)). +- Fixed inherited 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 inherited Markdown streaming code fence rendering so partial closing fences no longer make code blocks shrink or flicker while content streams ([#5846](https://github.com/earendil-works/pi/pull/5846) by [@xl0](https://github.com/xl0)). +- Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). +- Fixed bash 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)). +- Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). +- Fixed `/model` selector search to rank exact provider-prefixed matches before proxy-provider model ID matches ([#5892](https://github.com/earendil-works/pi/issues/5892)). + +## [0.79.8] - 2026-06-19 + +### New Features + +- **Selective provider base entry points** - SDK users can pair `@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` with explicit provider registration to keep bundled applications from including unused provider transports. See [`pi-ai` Base Entry Point](../ai/README.md#base-entry-point) and [`pi-agent-core` Base Entry Point](../agent/README.md#base-entry-point). +- **Mistral prompt caching** - Mistral sessions now use provider-side prompt caching with session affinity and cached-token usage/cost accounting. See [API Keys](docs/providers.md#api-keys) and [Environment Variables](docs/usage.md#environment-variables). +- **Post-compaction token estimates** - Compact results and compaction events now include estimated post-compaction token counts so clients can show the approximate context reduction. See [RPC compact](docs/rpc.md#compact) and [compaction events](docs/rpc.md#compaction_start--compaction_end). +- **OpenRouter Fusion alias** - `openrouter/fusion` is available as a built-in OpenRouter model alias. See [API Keys](docs/providers.md#api-keys). + +### Added + +- Added inherited `@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` entry points for selective provider registration in bundled applications ([#5348](https://github.com/earendil-works/pi/pull/5348) by [@FredKSchott](https://github.com/FredKSchott)). +- Added inherited Mistral prompt caching 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 estimated post-compaction token counts to compact results and compaction events ([#5877](https://github.com/earendil-works/pi/issues/5877)). +- Added the inherited OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). + +### Fixed + +- Updated vulnerable runtime dependencies, including `undici` and the packaged `protobufjs` transitive dependency. +- Fixed compaction to refuse sessions with no eligible messages instead of producing empty summaries ([#4811](https://github.com/earendil-works/pi/issues/4811)). +- Fixed successful overflow-triggered auto-compaction to avoid retrying completed assistant responses ([#5720](https://github.com/earendil-works/pi/issues/5720)). + +## [0.79.7] - 2026-06-18 + +### New Features + +- **Automatic theme mode** - `/settings` can choose separate light and dark themes and follow terminal color-scheme changes. See [Selecting a Theme](docs/themes.md#selecting-a-theme). +- **Self-only updates by default** - `pi update` now updates pi only, with `pi update --all` for updating pi and packages together. See [Install and Manage](docs/packages.md#install-and-manage). +- **Extension API helpers** - extensions can use `CONFIG_DIR_NAME` for project config paths and import edit diff helpers for edit-style diffs. See [`ctx.cwd`](docs/extensions.md#ctxcwd) and [SDK Exports](docs/sdk.md#exports). +- **Warp inline images** - Warp terminals now get inline image rendering through Kitty graphics detection. See [Image](docs/tui.md#image). + +### Added + +- Added automatic theme mode so `/settings` can use separate light and dark themes and follow terminal color-scheme changes ([#5874](https://github.com/earendil-works/pi/pull/5874)). +- Added inherited Warp terminal image capability detection so inline images render through Warp's Kitty graphics support ([#5841](https://github.com/earendil-works/pi/pull/5841) by [@dodiego](https://github.com/dodiego)). +- Exported `CONFIG_DIR_NAME` from the coding-agent public API so extensions can resolve project config paths without hardcoding `.pi` ([#5869](https://github.com/earendil-works/pi/pull/5869) by [@xl0](https://github.com/xl0)). +- Exported edit diff helpers (`generateDiffString`, `generateUnifiedPatch`, and `EditDiffResult`) from the public API for extensions that need edit-style diffs ([#5756](https://github.com/earendil-works/pi/pull/5756) by [@xl0](https://github.com/xl0)). + +### Changed + +- Changed bare `pi update` to update only pi, added `pi update --all` for updating pi and extensions together, and clarified extension update prompts. +- Reserved `/` in theme names for automatic light/dark theme settings. - Updated extension docs, examples, runtime help, trust prompts, and config labels to use the configured project config directory instead of hardcoded `.pi` paths. ### Fixed diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 41a7bbc2..765c7a2f 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -7,11 +7,6 @@ Discord npm

-

- pi.dev domain graciously donated by -

- Exy mascot
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). @@ -126,7 +121,7 @@ For each built-in provider, pi maintains a list of tool-capable models, updated - xAI - OpenRouter - Vercel AI Gateway -- ZAI +- ZAI Coding Plan (Global) - ZAI Coding Plan (China) - OpenCode Zen - OpenCode Go @@ -191,7 +186,8 @@ Type `/` in the editor to trigger commands. [Extensions](#extensions) can regist | `/clone` | Duplicate the current active branch into a new session | | `/compact [prompt]` | Manually compact context, optional custom instructions | | `/copy` | Copy last assistant message to clipboard | -| `/export [file]` | Export session to HTML file | +| `/export [file]` | Export session to HTML or JSONL file | +| `/import ` | Import and resume a session from a JSONL file | | `/share` | Upload as private GitHub gist with shareable HTML link | | `/reload` | Reload keybindings, extensions, skills, prompts, and context files (themes hot-reload automatically) | | `/hotkeys` | Show all keyboard shortcuts | @@ -419,7 +415,8 @@ pi install ssh://git@github.com/user/repo@v1 # tag or commit pi remove npm:@foo/pi-tools pi uninstall npm:@foo/pi-tools # alias for remove pi list -pi update # update pi and packages (skips pinned packages) +pi update # update pi only +pi update --all # update pi and packages pi update --extensions # update packages only pi update --self # update pi only pi update --self --force # reinstall pi even if current @@ -427,7 +424,7 @@ pi update npm:@foo/pi-tools # update one package pi config # enable/disable extensions, skills, prompts, themes ``` -Packages install to `~/.pi/agent/git/` (git) or `~/.pi/agent/npm/` (npm). Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git `@ref` values are pinned tags or commits; pinned packages are skipped by `pi update`, so use `pi install git:host/user/repo@new-ref` to move an existing package to a new ref. Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. +Packages install to `~/.pi/agent/git/` (git) or `~/.pi/agent/npm/` (npm). Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git `@ref` values are pinned tags or commits; pinned packages are skipped by `pi update --extensions` and `pi update --all`, so use `pi install git:host/user/repo@new-ref` to move an existing package to a new ref. Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. Create a package by adding a `pi` key to `package.json`: @@ -518,7 +515,8 @@ pi [options] [@files...] [messages...] pi install [-l] # Install package, -l for project-local pi remove [-l] # Remove package pi uninstall [-l] # Alias for remove -pi update [source|self|pi] # Update pi and packages (skips pinned packages) +pi update [source|self|pi] # Update pi only, or one package source +pi update --all # Update pi and packages pi update --extensions # Update packages only pi update --self # Update pi only pi update --self --force # Reinstall pi even if current @@ -673,8 +671,6 @@ pi --thinking high "Solve this complex problem" See [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines and [docs/development.md](docs/development.md) for setup, forking, and debugging. ---- - ## License MIT @@ -684,3 +680,9 @@ MIT - [@earendil-works/pi-ai](https://www.npmjs.com/package/@earendil-works/pi-ai): Core LLM toolkit - [@earendil-works/pi-agent-core](https://www.npmjs.com/package/@earendil-works/pi-agent-core): Agent framework - [@earendil-works/pi-tui](https://www.npmjs.com/package/@earendil-works/pi-tui): Terminal UI components + +

+ pi.dev domain graciously donated by +

+ Exy mascot
exe.dev
+

diff --git a/packages/coding-agent/docs/compaction.md b/packages/coding-agent/docs/compaction.md index babcddc6..5e0d4eef 100644 --- a/packages/coding-agent/docs/compaction.md +++ b/packages/coding-agent/docs/compaction.md @@ -276,7 +276,7 @@ Fired before auto-compaction or `/compact`. Can cancel or provide custom summary ```typescript pi.on("session_before_compact", async (event, ctx) => { - const { preparation, branchEntries, customInstructions, signal } = event; + const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event; // preparation.messagesToSummarize - messages to summarize // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn) @@ -287,6 +287,8 @@ pi.on("session_before_compact", async (event, ctx) => { // preparation.settings - compaction settings // branchEntries - all entries on current branch (for custom state) + // reason - "manual" (/compact), "threshold", or "overflow" + // willRetry - whether the aborted turn is retried after compaction (overflow recovery) // signal - AbortSignal (pass to LLM calls) // Cancel: diff --git a/packages/coding-agent/docs/custom-provider.md b/packages/coding-agent/docs/custom-provider.md index c2bf4455..612d1e60 100644 --- a/packages/coding-agent/docs/custom-provider.md +++ b/packages/coding-agent/docs/custom-provider.md @@ -229,7 +229,7 @@ models: [{ }] ``` -Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` instead for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`. +Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`. Use `cacheControlFormat: "anthropic"` for OpenAI-compatible providers that expose Anthropic-style prompt caching via `cache_control` on the system prompt, last tool definition, and last user/assistant text content. For Anthropic-compatible providers using `api: "anthropic-messages"`, set `compat.forceAdaptiveThinking: true` on models or providers whose upstream model requires adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`). Built-in adaptive Claude models set this automatically. Set `compat.allowEmptySignature: true` only for providers that emit empty thinking signatures and expect `signature: ""` on replay. @@ -718,7 +718,8 @@ interface ProviderModelConfig { requiresAssistantAfterToolResult?: boolean; requiresThinkingAsText?: boolean; requiresReasoningContentOnAssistantMessages?: boolean; - thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template"; + thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling"; + chatTemplateKwargs?: Record; cacheControlFormat?: "anthropic"; // anthropic-messages @@ -732,5 +733,5 @@ interface ProviderModelConfig { } ``` -`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`. +`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`. Use `chat-template` for configurable `chat_template_kwargs`, for example DeepSeek V3.x behind vLLM with `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }`. `cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content. diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 5a559813..a9271ee8 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -437,7 +437,10 @@ Fired on compaction. See [compaction.md](compaction.md) for details. ```typescript pi.on("session_before_compact", async (event, ctx) => { - const { preparation, branchEntries, customInstructions, signal } = event; + const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event; + + // reason - "manual" (/compact), "threshold", or "overflow" + // willRetry - whether the aborted turn is retried after compaction (overflow recovery) // Cancel: return { cancel: true }; @@ -455,6 +458,8 @@ pi.on("session_before_compact", async (event, ctx) => { pi.on("session_compact", async (event, ctx) => { // event.compactionEntry - the saved compaction // event.fromExtension - whether extension provided it + // event.reason - "manual" (/compact), "threshold", or "overflow" + // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery) }); ``` diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index 333b94e3..a15a25a0 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -54,7 +54,7 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1 | Keybinding id | Default | Description | |--------|---------|-------------| -| `tui.input.newLine` | `shift+enter` | Insert new line | +| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Insert new line | | `tui.input.submit` | `enter` | Submit input | | `tui.input.tab` | `tab` | Tab / autocomplete | diff --git a/packages/coding-agent/docs/models.md b/packages/coding-agent/docs/models.md index 830eaacd..d17e9928 100644 --- a/packages/coding-agent/docs/models.md +++ b/packages/coding-agent/docs/models.md @@ -34,7 +34,7 @@ For local models (Ollama, LM Studio, vLLM), only `id` is required per model: } ``` -The `apiKey` is required but Ollama ignores it, so any value works. +The `apiKey` value is a placeholder because Ollama ignores it. pi still treats models as requiring auth before they appear in `/model`, so keyless local servers should keep a dummy value, save a key for that provider with `/login`, or pass `--api-key` when selecting the model. Some OpenAI-compatible servers do not understand the `developer` role used for reasoning-capable models. For those providers, set `compat.supportsDeveloperRole` to `false` so pi sends the system prompt as a `system` message instead. If the server also does not support `reasoning_effort`, set `compat.supportsReasoningEffort` to `false` too. @@ -135,12 +135,14 @@ Set `api` at provider level (default for all models) or model level (override pe |-------|-------------| | `baseUrl` | API endpoint URL | | `api` | API type (see above) | -| `apiKey` | API key (see value resolution below) | +| `apiKey` | Optional API key config (see value resolution below). Omit it when auth is provided by `/login`/`auth.json` or CLI `--api-key`. | | `headers` | Custom headers (see value resolution below) | | `authHeader` | Set `true` to add `Authorization: Bearer ` automatically | | `models` | Array of model configurations | | `modelOverrides` | Per-model overrides for built-in models on this provider | +For providers with `models`, non-built-in provider configs need `baseUrl` and an `api` value at either provider or model level. `apiKey` is not required to load the file: models become available when auth is configured through `/login`/`auth.json`, CLI `--api-key`, or provider `apiKey`. If no auth is configured, the models load but stay unavailable in `/model` and `--list-models`. + ### Value Resolution The `apiKey` and `headers` fields support command execution, environment interpolation, and literals: @@ -399,14 +401,15 @@ For providers with partial OpenAI compatibility, use the `compat` field. | `requiresAssistantAfterToolResult` | Insert an assistant message before a user message after tool results | | `requiresThinkingAsText` | Convert thinking blocks to plain text | | `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled | -| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, or `qwen-chat-template` thinking parameters | +| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, `chat-template`, or `qwen-chat-template` thinking parameters | +| `chatTemplateKwargs` | `chat_template_kwargs` values for `thinkingFormat: "chat-template"`; use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values | | `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user/assistant text content. Currently only `anthropic` is supported. | | `supportsStrictMode` | Include the `strict` field in tool definitions | | `supportsLongCacheRetention` | Whether the provider accepts long cache retention when cache retention is `long`: `prompt_cache_retention: "24h"` for OpenAI prompt caching, or `cache_control.ttl: "1h"` when `cacheControlFormat` is `anthropic`. Default: `true`. | | `openRouterRouting` | OpenRouter provider routing preferences. This object is sent as-is in the `provider` field of the [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). | | `vercelGatewayRouting` | Vercel AI Gateway routing config for provider selection (`only`, `order`) | -`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking`. +`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking` and `preserve_thinking`. Use `chat-template` for vLLM/Hugging Face chat templates that need configurable `chat_template_kwargs`, such as `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }` for DeepSeek V3.x templates. `cacheControlFormat: "anthropic"` is for OpenAI-compatible providers that expose Anthropic-style prompt caching through `cache_control` markers on text content and tool definitions. diff --git a/packages/coding-agent/docs/packages.md b/packages/coding-agent/docs/packages.md index 7009b773..73f2dccb 100644 --- a/packages/coding-agent/docs/packages.md +++ b/packages/coding-agent/docs/packages.md @@ -28,7 +28,8 @@ pi install ./relative/path/to/package pi remove npm:@foo/bar pi list # show installed packages from settings -pi update # update pi, update packages, and reconcile pinned git refs +pi update # update pi only +pi update --all # update pi, update packages, and reconcile pinned git refs pi update --extensions # update packages and reconcile pinned git refs only pi update --self # update pi only pi update --self --force # reinstall pi even if current @@ -36,7 +37,7 @@ pi update npm:@foo/bar # update one package pi update --extension npm:@foo/bar ``` -These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). +These commands manage pi packages and `pi update` can update the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). By default, `install` and `remove` write to user settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup after the project is trusted. @@ -58,7 +59,7 @@ npm:@scope/pkg@1.2.3 npm:pkg ``` -- Versioned specs are pinned and skipped by package updates (`pi update`, `pi update --extensions`). +- Versioned specs are pinned and skipped by package updates (`pi update --extensions`, `pi update --all`). - User installs go under `~/.pi/agent/npm/`. - Project installs go under `.pi/npm/`. - Set `npmCommand` in `settings.json` to pin npm package lookup and install operations to a specific wrapper command such as `mise` or `asdf`. @@ -85,7 +86,7 @@ ssh://git@github.com/user/repo@v1 - HTTPS and SSH URLs are both supported. - SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`). - For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast. -- Refs are pinned tags or commits. `pi update` and `pi update --extensions` do not move them to newer refs, but they do reconcile an existing clone to the configured ref. +- Refs are pinned tags or commits. `pi update --extensions` and `pi update --all` do not move them to newer refs, but they do reconcile an existing clone to the configured ref. - Use `pi install git:host/user/repo@new-ref` to update settings and move an existing package to a new pinned ref. - Cloned to `~/.pi/agent/git//` (global) or `.pi/git//` (project). - When reconciliation changes the checkout, pi resets and cleans the clone, then runs `npm install` if `package.json` exists. diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index 86f1de06..46163abf 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -63,7 +63,7 @@ pi | xAI | `XAI_API_KEY` | `xai` | | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` | | Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` | -| ZAI | `ZAI_API_KEY` | `zai` | +| ZAI Coding Plan (Global) | `ZAI_API_KEY` | `zai` | | ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` | | OpenCode Zen | `OPENCODE_API_KEY` | `opencode` | | OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` | @@ -156,8 +156,9 @@ OAuth credentials are also stored here after `/login` and managed automatically. ```bash export AZURE_OPENAI_API_KEY=... -export AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com +export AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com # also supported: https://your-resource.cognitiveservices.azure.com +# also supported: https://your-resource.openai.azure.com # root endpoints are auto-normalized to /openai/v1 # or use resource name instead of base URL export AZURE_OPENAI_RESOURCE_NAME=your-resource diff --git a/packages/coding-agent/docs/rpc.md b/packages/coding-agent/docs/rpc.md index 9aa16ffc..a9942409 100644 --- a/packages/coding-agent/docs/rpc.md +++ b/packages/coding-agent/docs/rpc.md @@ -374,11 +374,14 @@ Response: "summary": "Summary of conversation...", "firstKeptEntryId": "abc123", "tokensBefore": 150000, + "estimatedTokensAfter": 32000, "details": {} } } ``` +`estimatedTokensAfter` is a heuristic estimate over the rebuilt message context immediately after compaction, not a provider-exact token count. + #### set_auto_compaction Enable or disable automatic compaction when context is nearly full. @@ -924,6 +927,7 @@ The `reason` field is `"manual"`, `"threshold"`, or `"overflow"`. "summary": "Summary of conversation...", "firstKeptEntryId": "abc123", "tokensBefore": 150000, + "estimatedTokensAfter": 32000, "details": {} }, "aborted": false, diff --git a/packages/coding-agent/docs/terminal-setup.md b/packages/coding-agent/docs/terminal-setup.md index ff1638b5..d2a8fb92 100644 --- a/packages/coding-agent/docs/terminal-setup.md +++ b/packages/coding-agent/docs/terminal-setup.md @@ -30,13 +30,7 @@ That mapping sends a raw linefeed byte. Inside pi, that is indistinguishable fro If Claude Code 2.x or newer is the only reason you added that mapping, you can remove it, unless you want to use Claude Code in tmux, where it still requires that Ghostty mapping. -If you want `Shift+Enter` to keep working in tmux via that remap, add `ctrl+j` to your pi `newLine` keybinding in `~/.pi/agent/keybindings.json`: - -```json -{ - "newLine": ["shift+enter", "ctrl+j"] -} -``` +Pi binds `Ctrl+J` as a default newline alias, so `Shift+Enter` keeps working in tmux via that remap without extra pi configuration. ## WezTerm diff --git a/packages/coding-agent/docs/themes.md b/packages/coding-agent/docs/themes.md index c18e954b..11655128 100644 --- a/packages/coding-agent/docs/themes.md +++ b/packages/coding-agent/docs/themes.md @@ -137,7 +137,7 @@ vim ~/.pi/agent/themes/my-theme.json } ``` -- `name` is required and must be unique. +- `name` is required, must be unique, and must not contain `/`. - `vars` is optional. Define reusable colors here, then reference them in `colors`. - `colors` must define all 51 required tokens. diff --git a/packages/coding-agent/docs/tui.md b/packages/coding-agent/docs/tui.md index c8ff3554..38ef1986 100644 --- a/packages/coding-agent/docs/tui.md +++ b/packages/coding-agent/docs/tui.md @@ -742,7 +742,7 @@ ctx.ui.setStatus("my-ext", ctx.ui.theme.fg("accent", "● active")); ctx.ui.setStatus("my-ext", undefined); ``` -**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts) +**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts) ### Pattern 4b: Working Indicator Customization @@ -802,7 +802,7 @@ ctx.ui.setWidget("my-widget", (_tui, theme) => { ctx.ui.setWidget("my-widget", undefined); ``` -**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts) +**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) ### Pattern 6: Custom Footer @@ -919,7 +919,7 @@ export default function (pi: ExtensionAPI) { - **Selection UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList with DynamicBorder framing - **Async with cancel**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader for LLM calls - **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable -- **Status indicators**: [examples/extensions/plan-mode.ts](../examples/extensions/plan-mode.ts) - setStatus and setWidget +- **Status indicators**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus and setWidget - **Working indicator**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator - **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats - **Custom editor**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Vim-like modal editing diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index 4f8f5954..ccd3ee41 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -44,11 +44,13 @@ Type `/` in the editor to open command completion. Extensions can register custo | `/name ` | Set session display name | | `/session` | Show session file, ID, messages, tokens, and cost | | `/tree` | Jump to any point in the session and continue from there | +| `/trust` | Save project trust decision for future sessions | | `/fork` | Create a new session from a previous user message | | `/clone` | Duplicate the current active branch into a new session | | `/compact [prompt]` | Manually compact context, optionally with custom instructions | | `/copy` | Copy last assistant message to clipboard | -| `/export [file]` | Export session to HTML | +| `/export [file]` | Export session to HTML or JSONL | +| `/import ` | Import and resume a session from a JSONL file | | `/share` | Upload as private GitHub gist with shareable HTML link | | `/reload` | Reload keybindings, extensions, skills, prompts, and context files | | `/hotkeys` | Show all keyboard shortcuts | @@ -145,7 +147,8 @@ pi [options] [@files...] [messages...] pi install [-l] # Install package, -l for project-local pi remove [-l] # Remove package pi uninstall [-l] # Alias for remove -pi update [source|self|pi] # Update pi and packages; reconcile pinned git refs +pi update [source|self|pi] # Update pi only, or one package source +pi update --all # Update pi and packages; reconcile pinned git refs pi update --extensions # Update packages only; reconcile pinned git refs pi update --self # Update pi only pi update --extension # Update one package @@ -153,7 +156,7 @@ pi list # List installed packages pi config # Enable/disable package resources ``` -These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). `pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust. +These commands manage pi packages and `pi update` can update the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). `pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust. See [Pi Packages](packages.md) for package sources and security notes. diff --git a/packages/coding-agent/examples/extensions/custom-compaction.ts b/packages/coding-agent/examples/extensions/custom-compaction.ts index 02c6ceb3..310e81d8 100644 --- a/packages/coding-agent/examples/extensions/custom-compaction.ts +++ b/packages/coding-agent/examples/extensions/custom-compaction.ts @@ -13,7 +13,7 @@ * pi --extension examples/extensions/custom-compaction.ts */ -import { complete } from "@earendil-works/pi-ai"; +import { complete } from "@earendil-works/pi-ai/compat"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent"; diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts b/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts index 51426362..cfa80dbe 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts @@ -153,7 +153,7 @@ async function refreshAnthropicToken(credentials: OAuthCredentials): Promise), compat: { @@ -336,7 +336,11 @@ export function streamGitLabDuo( context, streamOptions, ) - : streamSimpleOpenAIResponses(modelWithBaseUrl as Model<"openai-responses">, context, streamOptions); + : openAIResponsesApi().streamSimple( + modelWithBaseUrl as Model<"openai-responses">, + context, + streamOptions, + ); for await (const event of innerStream) stream.push(event); stream.end(); diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 0478f9c8..38f4684a 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.6", + "version": "0.80.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts index 0077aca8..79ba3d71 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts @@ -8,7 +8,7 @@ * npx tsx test.ts claude-sonnet-4-5-20250929 --thinking */ -import { type Api, type Context, type Model, registerApiProvider, streamSimple } from "@earendil-works/pi-ai"; +import { type Api, type Context, type Model, registerApiProvider, streamSimple } from "@earendil-works/pi-ai/compat"; import { readFileSync } from "fs"; import { getAgentDir } from "packages/coding-agent/src/config.js"; import { join } from "path"; diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index c573e8e0..b29a04e9 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.6", + "version": "0.80.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.6", + "version": "0.80.2", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 71e89d6c..9cbb476e 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.6", + "version": "0.80.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/handoff.ts b/packages/coding-agent/examples/extensions/handoff.ts index 4af6661f..a161fad6 100644 --- a/packages/coding-agent/examples/extensions/handoff.ts +++ b/packages/coding-agent/examples/extensions/handoff.ts @@ -13,7 +13,7 @@ */ import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { complete, type Message } from "@earendil-works/pi-ai"; +import { complete, type Message } from "@earendil-works/pi-ai/compat"; import type { ExtensionAPI, SessionEntry } from "@earendil-works/pi-coding-agent"; import { BorderedLoader, convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent"; diff --git a/packages/coding-agent/examples/extensions/plan-mode/README.md b/packages/coding-agent/examples/extensions/plan-mode/README.md index 549e3473..2568a684 100644 --- a/packages/coding-agent/examples/extensions/plan-mode/README.md +++ b/packages/coding-agent/examples/extensions/plan-mode/README.md @@ -4,7 +4,7 @@ Read-only exploration mode for safe code analysis. ## Features -- **Read-only tools**: Restricts available tools to read, bash, grep, find, ls, question +- **Built-in write tools disabled**: Disables edit/write while preserving other active tools - **Bash allowlist**: Only read-only bash commands are allowed - **Plan extraction**: Extracts numbered steps from `Plan:` sections - **Progress tracking**: Widget shows completion status during execution @@ -37,7 +37,8 @@ Plan: ## How It Works ### Plan Mode (Read-Only) -- Only read-only tools available +- Built-in edit/write tools disabled +- Other active tools remain available - Bash commands filtered through allowlist - Agent creates a plan without making changes diff --git a/packages/coding-agent/examples/extensions/plan-mode/index.ts b/packages/coding-agent/examples/extensions/plan-mode/index.ts index 40db408c..737ce56a 100644 --- a/packages/coding-agent/examples/extensions/plan-mode/index.ts +++ b/packages/coding-agent/examples/extensions/plan-mode/index.ts @@ -2,7 +2,7 @@ * Plan Mode Extension * * Read-only exploration mode for safe code analysis. - * When enabled, only read-only tools are available. + * When enabled, built-in write tools are disabled. * * Features: * - /plan command or Ctrl+Alt+P to toggle @@ -21,6 +21,15 @@ import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } fr // Tools const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"]; const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"]; +const PLAN_MODE_DISABLED_TOOLS = new Set(["edit", "write"]); +const PLAN_MANAGED_TOOLS = new Set([...PLAN_MODE_TOOLS, ...NORMAL_MODE_TOOLS]); + +interface PlanModeState { + enabled: boolean; + todos?: TodoItem[]; + executing?: boolean; + toolsBeforePlanMode?: string[]; +} // Type guard for assistant messages function isAssistantMessage(m: AgentMessage): m is AssistantMessage { @@ -39,6 +48,7 @@ export default function planModeExtension(pi: ExtensionAPI): void { let planModeEnabled = false; let executionMode = false; let todoItems: TodoItem[] = []; + let toolsBeforePlanMode: string[] | undefined; pi.registerFlag("plan", { description: "Start in plan mode (read-only exploration)", @@ -73,19 +83,34 @@ export default function planModeExtension(pi: ExtensionAPI): void { } } - function togglePlanMode(ctx: ExtensionContext): void { - planModeEnabled = !planModeEnabled; - executionMode = false; - todoItems = []; + function uniqueToolNames(toolNames: string[]): string[] { + return [...new Set(toolNames)]; + } - if (planModeEnabled) { - pi.setActiveTools(PLAN_MODE_TOOLS); - ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`); - } else { - pi.setActiveTools(NORMAL_MODE_TOOLS); - ctx.ui.notify("Plan mode disabled. Full access restored."); + function getPlanModeTools(activeToolNames: string[]): string[] { + return uniqueToolNames([ + ...activeToolNames.filter((name) => !PLAN_MODE_DISABLED_TOOLS.has(name)), + ...PLAN_MODE_TOOLS, + ]); + } + + function getNormalModeTools(activeToolNames: string[]): string[] { + return uniqueToolNames([ + ...NORMAL_MODE_TOOLS, + ...activeToolNames.filter((name) => !PLAN_MANAGED_TOOLS.has(name)), + ]); + } + + function enablePlanModeTools(): void { + if (toolsBeforePlanMode === undefined) { + toolsBeforePlanMode = pi.getActiveTools(); } - updateStatus(ctx); + pi.setActiveTools(getPlanModeTools(toolsBeforePlanMode)); + } + + function restoreNormalModeTools(): void { + pi.setActiveTools(toolsBeforePlanMode ?? getNormalModeTools(pi.getActiveTools())); + toolsBeforePlanMode = undefined; } function persistState(): void { @@ -93,9 +118,26 @@ export default function planModeExtension(pi: ExtensionAPI): void { enabled: planModeEnabled, todos: todoItems, executing: executionMode, + toolsBeforePlanMode, }); } + function togglePlanMode(ctx: ExtensionContext): void { + planModeEnabled = !planModeEnabled; + executionMode = false; + todoItems = []; + + if (planModeEnabled) { + enablePlanModeTools(); + ctx.ui.notify("Plan mode enabled. Built-in write tools disabled."); + } else { + restoreNormalModeTools(); + ctx.ui.notify("Plan mode disabled. Full access restored."); + } + updateStatus(ctx); + persistState(); + } + pi.registerCommand("plan", { description: "Toggle plan mode (read-only exploration)", handler: async (_args, ctx) => togglePlanMode(ctx), @@ -165,8 +207,8 @@ export default function planModeExtension(pi: ExtensionAPI): void { You are in plan mode - a read-only exploration mode for safe code analysis. Restrictions: -- You can only use: read, bash, grep, find, ls, questionnaire -- You CANNOT use: edit, write (file modifications are disabled) +- Built-in edit and write tools are disabled +- Other currently active tools remain available - Bash is restricted to an allowlist of read-only commands Ask clarifying questions using the questionnaire tool. @@ -228,7 +270,6 @@ After completing a step, include a [DONE:n] tag in your response.`, ); executionMode = false; todoItems = []; - pi.setActiveTools(NORMAL_MODE_TOOLS); updateStatus(ctx); persistState(); // Save cleared state so resume doesn't restore old execution mode } @@ -246,43 +287,51 @@ After completing a step, include a [DONE:n] tag in your response.`, } } + if (todoItems.length === 0) return; + persistState(); + // Show plan steps and prompt for next action - if (todoItems.length > 0) { - const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n"); - pi.sendMessage( - { - customType: "plan-todo-list", - content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`, - display: true, - }, - { triggerTurn: false }, - ); - } + const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n"); + const planTodoListMessage = { + customType: "plan-todo-list", + content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`, + display: true, + }; const choice = await ctx.ui.select("Plan mode - what next?", [ - todoItems.length > 0 ? "Execute the plan (track progress)" : "Execute the plan", + "Execute the plan (track progress)", "Stay in plan mode", "Refine the plan", ]); if (choice?.startsWith("Execute")) { - planModeEnabled = false; - executionMode = todoItems.length > 0; - pi.setActiveTools(NORMAL_MODE_TOOLS); - updateStatus(ctx); + const firstTodoItem = todoItems[0]; + if (!firstTodoItem) return; - const execMessage = - todoItems.length > 0 - ? `Execute the plan. Start with: ${todoItems[0].text}` - : "Execute the plan you just created."; + planModeEnabled = false; + executionMode = true; + restoreNormalModeTools(); + updateStatus(ctx); + persistState(); + + const remainingList = todoItems.map((t) => `${t.step}. ${t.text}`).join("\n"); + const execMessage = `Execute the plan. + +Remaining steps: +${remainingList} + +Start with: ${firstTodoItem.text} +After completing a step, include a [DONE:n] tag in your response.`; + pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" }); pi.sendMessage( { customType: "plan-mode-execute", content: execMessage, display: true }, - { triggerTurn: true }, + { triggerTurn: true, deliverAs: "followUp" }, ); } else if (choice === "Refine the plan") { const refinement = await ctx.ui.editor("Refine the plan:", ""); if (refinement?.trim()) { - pi.sendUserMessage(refinement.trim()); + pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" }); + pi.sendUserMessage(refinement.trim(), { deliverAs: "followUp" }); } } }); @@ -298,12 +347,13 @@ After completing a step, include a [DONE:n] tag in your response.`, // Restore persisted state const planModeEntry = entries .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode") - .pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } | undefined; + .pop() as { data?: PlanModeState } | undefined; if (planModeEntry?.data) { planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled; todoItems = planModeEntry.data.todos ?? todoItems; executionMode = planModeEntry.data.executing ?? executionMode; + toolsBeforePlanMode = planModeEntry.data.toolsBeforePlanMode ?? toolsBeforePlanMode; } // On resume: re-scan messages to rebuild completion state @@ -333,7 +383,7 @@ After completing a step, include a [DONE:n] tag in your response.`, } if (planModeEnabled) { - pi.setActiveTools(PLAN_MODE_TOOLS); + enablePlanModeTools(); } updateStatus(ctx); }); diff --git a/packages/coding-agent/examples/extensions/qna.ts b/packages/coding-agent/examples/extensions/qna.ts index 70dbef7b..524c2785 100644 --- a/packages/coding-agent/examples/extensions/qna.ts +++ b/packages/coding-agent/examples/extensions/qna.ts @@ -7,7 +7,7 @@ * 3. Loads the result into the editor for user to fill in answers */ -import { complete, type UserMessage } from "@earendil-works/pi-ai"; +import { complete, type UserMessage } from "@earendil-works/pi-ai/compat"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { BorderedLoader } from "@earendil-works/pi-coding-agent"; diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 24e20622..71528853 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.6", + "version": "1.10.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.6", + "version": "1.10.2", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 7f1fa016..f3cc2ac4 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.6", + "version": "1.10.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/summarize.ts b/packages/coding-agent/examples/extensions/summarize.ts index e6480974..66ce5340 100644 --- a/packages/coding-agent/examples/extensions/summarize.ts +++ b/packages/coding-agent/examples/extensions/summarize.ts @@ -1,4 +1,4 @@ -import { complete, getModel } from "@earendil-works/pi-ai"; +import { complete, getModel } from "@earendil-works/pi-ai/compat"; import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import { DynamicBorder, getMarkdownTheme } from "@earendil-works/pi-coding-agent"; import { Container, Markdown, matchesKey, Text } from "@earendil-works/pi-tui"; diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index fb5d53cd..0eb59503 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.6", + "version": "0.80.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.6", + "version": "0.80.2", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index b8d759de..53e2272f 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.6", + "version": "0.80.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/sdk/02-custom-model.ts b/packages/coding-agent/examples/sdk/02-custom-model.ts index 531a51c8..641d553e 100644 --- a/packages/coding-agent/examples/sdk/02-custom-model.ts +++ b/packages/coding-agent/examples/sdk/02-custom-model.ts @@ -4,7 +4,7 @@ * Shows how to select a specific model and thinking level. */ -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { AuthStorage, createAgentSession, ModelRegistry } from "@earendil-works/pi-coding-agent"; // Set up auth storage and model registry diff --git a/packages/coding-agent/examples/sdk/12-full-control.ts b/packages/coding-agent/examples/sdk/12-full-control.ts index cd8ba343..12b7607d 100644 --- a/packages/coding-agent/examples/sdk/12-full-control.ts +++ b/packages/coding-agent/examples/sdk/12-full-control.ts @@ -4,7 +4,7 @@ * Replace everything - no discovery, explicit configuration. */ -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { AuthStorage, createAgentSession, diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 39313f9d..467adb3b 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.80.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.80.2", "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", + "@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", @@ -25,7 +25,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" }, "optionalDependencies": { @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.6", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.6.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.2.tgz", "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" @@ -488,15 +488,16 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.6", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.6.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.2.tgz", "license": "MIT", "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", @@ -511,8 +512,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.6", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.6.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.2.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", @@ -741,14 +742,23 @@ "optional": true }, "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/@nodable/entities": { @@ -763,6 +773,24 @@ } ] }, + "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/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -782,9 +810,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": { @@ -802,12 +830,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", @@ -1585,23 +1607,22 @@ } }, "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==", "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" @@ -1707,9 +1728,9 @@ "license": "MIT" }, "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" @@ -1746,9 +1767,9 @@ } }, "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", "peerDependencies": { "bufferutil": "^4.0.1", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 22b408ce..8175de4f 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.80.2", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.6", - "@earendil-works/pi-ai": "^0.79.6", - "@earendil-works/pi-tui": "^0.79.6", + "@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", @@ -52,7 +52,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" }, "overrides": { @@ -74,7 +74,7 @@ "@types/semver": "7.7.1", "shx": "0.4.0", "typescript": "5.9.3", - "vitest": "3.2.4" + "vitest": "4.1.9" }, "keywords": [ "coding-agent", diff --git a/packages/coding-agent/src/bun/register-bedrock.ts b/packages/coding-agent/src/bun/register-bedrock.ts index 92af0dde..18e80dcc 100644 --- a/packages/coding-agent/src/bun/register-bedrock.ts +++ b/packages/coding-agent/src/bun/register-bedrock.ts @@ -1,4 +1,4 @@ -import { setBedrockProviderModule } from "@earendil-works/pi-ai"; import { bedrockProviderModule } from "@earendil-works/pi-ai/bedrock-provider"; +import { setBedrockProviderModule } from "@earendil-works/pi-ai/compat"; setBedrockProviderModule(bedrockProviderModule); diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 839c60e8..899241c4 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -229,7 +229,7 @@ ${chalk.bold("Commands:")} ${APP_NAME} install [-l] Install extension source and add to settings ${APP_NAME} remove [-l] Remove extension source from settings ${APP_NAME} uninstall [-l] Alias for remove - ${APP_NAME} update [source|self|pi] Update pi and installed extensions + ${APP_NAME} update [source|self|pi] Update pi (use --all for pi and extensions) ${APP_NAME} list List installed extensions from settings ${APP_NAME} config Open TUI to enable/disable package resources ${APP_NAME} --help Show help for install/remove/uninstall/update/list @@ -352,7 +352,7 @@ ${chalk.bold("Environment Variables:")} TOGETHER_API_KEY - Together AI API key OPENROUTER_API_KEY - OpenRouter API key AI_GATEWAY_API_KEY - Vercel AI Gateway API key - ZAI_API_KEY - ZAI API key + ZAI_API_KEY - ZAI Coding Plan API key (Global) ZAI_CODING_CN_API_KEY - ZAI Coding Plan API key (China) MISTRAL_API_KEY - Mistral API key MINIMAX_API_KEY - MiniMax API key diff --git a/packages/coding-agent/src/cli/session-picker.ts b/packages/coding-agent/src/cli/session-picker.ts index 42dcb808..793f1d0a 100644 --- a/packages/coding-agent/src/cli/session-picker.ts +++ b/packages/coding-agent/src/cli/session-picker.ts @@ -2,10 +2,12 @@ * TUI session selector for --resume flag */ -import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; +import { setKeybindings } from "@earendil-works/pi-tui"; import { KeybindingsManager } from "../core/keybindings.ts"; import type { SessionInfo, SessionListProgress } from "../core/session-manager.ts"; +import type { SettingsManager } from "../core/settings-manager.ts"; import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.ts"; +import { createStartupTui, startStartupTui } from "./startup-ui.ts"; type SessionsLoader = (onProgress?: SessionListProgress) => Promise; @@ -13,9 +15,10 @@ type SessionsLoader = (onProgress?: SessionListProgress) => Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = new TUI(new ProcessTerminal()); const keybindings = KeybindingsManager.create(); setKeybindings(keybindings); let resolved = false; @@ -47,6 +50,6 @@ export async function selectSession( ui.addChild(selector); ui.setFocus(selector.getSessionList()); - ui.start(); + startStartupTui(ui, settingsManager); }); } diff --git a/packages/coding-agent/src/cli/startup-ui.ts b/packages/coding-agent/src/cli/startup-ui.ts index 93841304..73c0271c 100644 --- a/packages/coding-agent/src/cli/startup-ui.ts +++ b/packages/coding-agent/src/cli/startup-ui.ts @@ -1,16 +1,27 @@ import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; import { existsSync } from "fs"; -import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, getSettingsPath, PACKAGE_NAME } from "../config.ts"; +import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, getAgentDir, getSettingsPath, PACKAGE_NAME } from "../config.ts"; import { areExperimentalFeaturesEnabled } from "../core/experimental.ts"; import { KeybindingsManager } from "../core/keybindings.ts"; -import type { SettingsManager } from "../core/settings-manager.ts"; +import { DefaultPackageManager, type ResolvedResource } from "../core/package-manager.ts"; +import { SettingsManager } from "../core/settings-manager.ts"; import { ExtensionInputComponent } from "../modes/interactive/components/extension-input.ts"; import { ExtensionSelectorComponent } from "../modes/interactive/components/extension-selector.ts"; import { FirstTimeSetupComponent, type FirstTimeSetupResult, } from "../modes/interactive/components/first-time-setup.ts"; -import { detectTerminalBackgroundTheme, initTheme, setTheme } from "../modes/interactive/theme/theme.ts"; +import { + detectTerminalBackgroundFromEnv, + detectTerminalThemeForAuto, + initTheme, + loadThemeFromPath, + parseAutoThemeSetting, + resolveThemeSetting, + setRegisteredThemes, + setTheme, + type Theme, +} from "../modes/interactive/theme/theme.ts"; const OFFICIAL_PACKAGE_NAME = "@earendil-works/pi-coding-agent"; const OFFICIAL_APP_NAME = "pi"; @@ -30,14 +41,64 @@ function isOfficialDistribution({ packageName, appName, configDirName }: Distrib ); } -function createStartupTui(settingsManager: SettingsManager): TUI { - initTheme(settingsManager.getTheme()); +function loadThemes(resources: ResolvedResource[]): Theme[] { + const themes: Theme[] = []; + const seen = new Set(); + for (const resource of resources) { + if (!resource.enabled) continue; + try { + const loadedTheme = loadThemeFromPath(resource.path); + if (loadedTheme.name) { + if (seen.has(loadedTheme.name)) continue; + seen.add(loadedTheme.name); + } + themes.push(loadedTheme); + } catch { + // Startup prompts should not fail because a theme is broken. The normal + // resource loader reports theme diagnostics later in startup. + } + } + return themes; +} + +async function loadStartupThemes(settingsManager: SettingsManager): Promise { + const globalSettingsManager = SettingsManager.inMemory(settingsManager.getGlobalSettings(), { + projectTrusted: false, + }); + const packageManager = new DefaultPackageManager({ + cwd: process.cwd(), + agentDir: getAgentDir(), + settingsManager: globalSettingsManager, + }); + const resolvedPaths = await packageManager.resolve(async () => "skip"); + return loadThemes(resolvedPaths.themes); +} + +export async function createStartupTui(settingsManager: SettingsManager): Promise { + setRegisteredThemes(await loadStartupThemes(settingsManager)); + const terminalTheme = detectTerminalBackgroundFromEnv().theme; + initTheme(resolveThemeSetting(settingsManager.getThemeSetting(), terminalTheme) ?? terminalTheme); setKeybindings(KeybindingsManager.create()); const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor()); ui.setClearOnShrink(settingsManager.getClearOnShrink()); return ui; } +export function startStartupTui(ui: TUI, settingsManager: SettingsManager): void { + ui.start(); + void applyDetectedStartupTheme(ui, settingsManager); +} + +async function applyDetectedStartupTheme(ui: TUI, settingsManager: SettingsManager): Promise { + const themeSetting = settingsManager.getThemeSetting(); + if (themeSetting && !parseAutoThemeSetting(themeSetting)) return; + + const terminalTheme = await detectTerminalThemeForAuto({ ui, timeoutMs: 100 }); + setTheme(resolveThemeSetting(themeSetting, terminalTheme) ?? terminalTheme); + ui.invalidate(); + ui.requestRender(); +} + async function clearStartupTui(ui: TUI): Promise { ui.clear(); ui.requestRender(); @@ -75,9 +136,8 @@ export async function showStartupSelector( title: string, options: Array<{ label: string; value: T }>, ): Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = createStartupTui(settingsManager); - let settled = false; const finish = async (result: T | undefined) => { if (settled) { @@ -98,15 +158,14 @@ export async function showStartupSelector( ); ui.addChild(selector); ui.setFocus(selector); - ui.start(); + startStartupTui(ui, settingsManager); }); } /** Show the first-time setup dialog and persist the result */ export async function showFirstTimeSetup(settingsManager: SettingsManager): Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = createStartupTui(settingsManager); - let settled = false; const finish = async (result: FirstTimeSetupResult | undefined) => { if (settled) { @@ -125,10 +184,10 @@ export async function showFirstTimeSetup(settingsManager: SettingsManager): Prom const showSetup = async () => { ui.start(); - const detection = await detectTerminalBackgroundTheme({ ui, timeoutMs: 100 }); - setTheme(detection.theme); + const detectedTheme = await detectTerminalThemeForAuto({ ui, timeoutMs: 100 }); + setTheme(detectedTheme); const component = new FirstTimeSetupComponent({ - detectedTheme: detection.theme, + detectedTheme, onThemePreview: (themeName) => { setTheme(themeName); ui.requestRender(); @@ -150,9 +209,8 @@ export async function showStartupInput( title: string, placeholder?: string, ): Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = createStartupTui(settingsManager); - let settled = false; const finish = async (result: string | undefined) => { if (settled) { @@ -176,6 +234,6 @@ export async function showStartupInput( ); ui.addChild(input); ui.setFocus(input); - ui.start(); + startStartupTui(ui, settingsManager); }); } diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 75b2efaf..38049f05 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -38,6 +38,18 @@ export interface SelfUpdateCommand extends SelfUpdateCommandStep { steps?: SelfUpdateCommandStep[]; } +export type SelfUpdatePackageTarget = string | { packageName: string; installSpec?: string }; + +function normalizeSelfUpdatePackageTarget(target: SelfUpdatePackageTarget): { + packageName: string; + installSpec: string; +} { + if (typeof target === "string") { + return { packageName: target, installSpec: target }; + } + return { packageName: target.packageName, installSpec: target.installSpec ?? target.packageName }; +} + function makeSelfUpdateCommand( installStep: SelfUpdateCommandStep, uninstallStep?: SelfUpdateCommandStep, @@ -103,9 +115,10 @@ function getInferredNpmInstall(): { root: string; prefix: string } | undefined { function getSelfUpdateCommandForMethod( method: InstallMethod, installedPackageName: string, - updatePackageName = installedPackageName, + updatePackageTarget: SelfUpdatePackageTarget = installedPackageName, npmCommand?: string[], ): SelfUpdateCommand | undefined { + const target = normalizeSelfUpdatePackageTarget(updatePackageTarget); switch (method) { case "bun-binary": return undefined; @@ -123,17 +136,17 @@ function getSelfUpdateCommandForMethod( "--ignore-scripts", "--config.minimumReleaseAge=0", ...binDirArgs, - updatePackageName, + target.installSpec, ]), - updatePackageName === installedPackageName + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", ...binDirArgs, installedPackageName]), ); } case "yarn": return makeSelfUpdateCommand( - makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]), - updatePackageName === installedPackageName + makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", target.installSpec]), + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]), ); @@ -144,9 +157,9 @@ function getSelfUpdateCommandForMethod( "-g", "--ignore-scripts", "--minimum-release-age=0", - updatePackageName, + target.installSpec, ]), - updatePackageName === installedPackageName + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]), ); @@ -160,10 +173,10 @@ function getSelfUpdateCommandForMethod( "-g", "--ignore-scripts", "--min-release-age=0", - updatePackageName, + target.installSpec, ]); const uninstallStep = - updatePackageName === installedPackageName + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]); return makeSelfUpdateCommand(installStep, uninstallStep); @@ -302,10 +315,10 @@ function isManagedByGlobalPackageManager(method: InstallMethod, packageName: str export function getSelfUpdateCommand( packageName: string, npmCommand?: string[], - updatePackageName = packageName, + updatePackageTarget: SelfUpdatePackageTarget = packageName, ): SelfUpdateCommand | undefined { const method = detectInstallMethod(); - const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); + const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageTarget, npmCommand); if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) { return undefined; } @@ -315,20 +328,21 @@ export function getSelfUpdateCommand( export function getSelfUpdateUnavailableInstruction( packageName: string, npmCommand?: string[], - updatePackageName = packageName, + updatePackageTarget: SelfUpdatePackageTarget = packageName, ): string { const method = detectInstallMethod(); + const target = normalizeSelfUpdatePackageTarget(updatePackageTarget); if (method === "bun-binary") { return `Download from: https://github.com/earendil-works/pi-mono/releases/latest`; } - const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); + const command = getSelfUpdateCommandForMethod(method, packageName, target, npmCommand); if (command) { if (isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) { return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`; } return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`; } - return `Update ${updatePackageName} using the package manager, wrapper, or source checkout that provides this installation.`; + return `Update ${target.installSpec} using the package manager, wrapper, or source checkout that provides this installation.`; } export function getUpdateInstruction(packageName: string): string { diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 54de3103..fafa1031 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -23,16 +23,17 @@ import type { AgentTool, ThinkingLevel, } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@earendil-works/pi-ai"; +import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@earendil-works/pi-ai/compat"; import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, + isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { getThemeByName, theme } from "../modes/interactive/theme/theme.ts"; import { stripFrontmatter } from "../utils/frontmatter.ts"; import { resolvePath } from "../utils/paths.ts"; @@ -45,6 +46,7 @@ import { collectEntriesForBranchSummary, compact, estimateContextTokens, + estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, @@ -242,6 +244,14 @@ interface ToolDefinitionEntry { sourceInfo: SourceInfo; } +function estimateMessagesTokens(messages: AgentMessage[]): number { + let tokens = 0; + for (const message of messages) { + tokens += estimateTokens(message); + } + return tokens; +} + // ============================================================================ // Constants // ============================================================================ @@ -1675,6 +1685,8 @@ export class AgentSession { preparation, branchEntries: pathEntries, customInstructions, + reason: "manual", + willRetry: false, signal: this._compactionAbortController.signal, })) as SessionBeforeCompactResult | undefined; @@ -1726,6 +1738,7 @@ export class AgentSession { const newEntries = this.sessionManager.getEntries(); const sessionContext = this.sessionManager.buildSessionContext(); this.agent.state.messages = sessionContext.messages; + const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages); // Get the saved compaction entry for the extension event const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary) as @@ -1737,13 +1750,16 @@ export class AgentSession { type: "session_compact", compactionEntry: savedCompactionEntry, fromExtension, + reason: "manual", + willRetry: false, }); } - const compactionResult = { + const compactionResult: CompactionResult = { summary, firstKeptEntryId, tokensBefore, + estimatedTokensAfter, details, }; this._emit({ @@ -1824,8 +1840,17 @@ export class AgentSession { return false; } - // Case 1: Overflow - LLM returned context overflow error + // Case 1: Overflow - LLM returned context overflow error, or reported usage exceeded + // the configured window. A successful response over the configured window should compact + // but must not retry: the assistant answer already completed and agent.continue() cannot + // continue from an assistant message. if (sameModel && isContextOverflow(assistantMessage, contextWindow)) { + const willRetry = assistantMessage.stopReason !== "stop"; + + if (!willRetry) { + return await this._runAutoCompaction("overflow", false); + } + if (this._overflowRecoveryAttempted) { this._emit({ type: "compaction_end", @@ -1846,14 +1871,16 @@ export class AgentSession { if (messages.length > 0 && messages[messages.length - 1].role === "assistant") { this.agent.state.messages = messages.slice(0, -1); } - return await this._runAutoCompaction("overflow", true); + return await this._runAutoCompaction("overflow", willRetry); } // Case 2: Threshold - context is getting large - // For error messages (no usage data), estimate from last successful response. - // This ensures sessions that hit persistent API errors (e.g. 529) can still compact. + // For error messages or all-zero usage messages, estimate from the last valid response. + // This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage + // responses can still compact and do not reset context accounting. let contextTokens: number; - if (assistantMessage.stopReason === "error") { + const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0; + if (assistantMessage.stopReason === "error" || directContextTokens === 0) { const messages = this.agent.state.messages; const estimate = estimateContextTokens(messages); if (estimate.lastUsageIndex === null) return false; // No usage data at all @@ -1870,7 +1897,7 @@ export class AgentSession { } contextTokens = estimate.tokens; } else { - contextTokens = calculateContextTokens(assistantMessage.usage); + contextTokens = directContextTokens; } if (shouldCompact(contextTokens, contextWindow, settings)) { return await this._runAutoCompaction("threshold", false); @@ -1883,19 +1910,10 @@ export class AgentSession { */ private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise { const settings = this.settingsManager.getCompactionSettings(); - - this._emit({ type: "compaction_start", reason }); - this._autoCompactionAbortController = new AbortController(); + let started = false; try { if (!this.model) { - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - }); return false; } @@ -1905,13 +1923,6 @@ export class AgentSession { if (this.agent.streamFn === streamSimple) { const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model); if (!authResult.ok || !authResult.apiKey) { - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - }); return false; } apiKey = authResult.apiKey; @@ -1925,16 +1936,13 @@ export class AgentSession { const preparation = prepareCompaction(pathEntries, settings); if (!preparation) { - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - }); return false; } + this._emit({ type: "compaction_start", reason }); + this._autoCompactionAbortController = new AbortController(); + started = true; + let extensionCompaction: CompactionResult | undefined; let fromExtension = false; @@ -1944,6 +1952,8 @@ export class AgentSession { preparation, branchEntries: pathEntries, customInstructions: undefined, + reason, + willRetry, signal: this._autoCompactionAbortController.signal, })) as SessionBeforeCompactResult | undefined; @@ -2009,6 +2019,7 @@ export class AgentSession { const newEntries = this.sessionManager.getEntries(); const sessionContext = this.sessionManager.buildSessionContext(); this.agent.state.messages = sessionContext.messages; + const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages); // Get the saved compaction entry for the extension event const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary) as @@ -2020,6 +2031,8 @@ export class AgentSession { type: "session_compact", compactionEntry: savedCompactionEntry, fromExtension, + reason, + willRetry, }); } @@ -2027,6 +2040,7 @@ export class AgentSession { summary, firstKeptEntryId, tokensBefore, + estimatedTokensAfter, details, }; this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry }); @@ -2045,17 +2059,19 @@ export class AgentSession { return this.agent.hasQueuedMessages(); } catch (error) { const errorMessage = error instanceof Error ? error.message : "compaction failed"; - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - errorMessage: - reason === "overflow" - ? `Context overflow recovery failed: ${errorMessage}` - : `Auto-compaction failed: ${errorMessage}`, - }); + if (started) { + this._emit({ + type: "compaction_end", + reason, + result: undefined, + aborted: false, + willRetry: false, + errorMessage: + reason === "overflow" + ? `Context overflow recovery failed: ${errorMessage}` + : `Auto-compaction failed: ${errorMessage}`, + }); + } return false; } finally { this._autoCompactionAbortController = undefined; @@ -2438,7 +2454,7 @@ export class AgentSession { }); } - async reload(): Promise { + async reload(options?: { beforeSessionStart?: () => void | Promise }): Promise { const previousFlagValues = this._extensionRunner.getFlagValues(); await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" }); await this.settingsManager.reload(); @@ -2457,6 +2473,7 @@ export class AgentSession { this._extensionShutdownHandler || this._extensionErrorListener; if (hasBindings) { + await options?.beforeSessionStart?.(); await this._extensionRunner.emit({ type: "session_start", reason: "reload" }); await this.extendResourcesFromExtensions("reload"); } @@ -2466,29 +2483,14 @@ export class AgentSession { // Auto-Retry // ========================================================================= - private _isNonRetryableProviderLimitError(errorMessage: string): boolean { - return /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test( - errorMessage, - ); - } - /** * Check if an error is retryable (overloaded, rate limit, server errors). * Context overflow errors are NOT retryable (handled by compaction instead). */ private _isRetryableError(message: AssistantMessage): boolean { - if (message.stopReason !== "error" || !message.errorMessage) return false; - - // Context overflow is handled by compaction, not retry - const contextWindow = this.model?.contextWindow ?? 0; - if (isContextOverflow(message, contextWindow)) return false; - - const err = message.errorMessage; - if (this._isNonRetryableProviderLimitError(err)) return false; - // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded - return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test( - err, - ); + // Context overflow is handled by compaction, not retry. + if (isContextOverflow(message, this.model?.contextWindow ?? 0)) return false; + return isRetryableAssistantError(message); } /** @@ -2997,8 +2999,8 @@ export class AgentSession { const contextTokens = calculateContextTokens(assistant.usage); if (contextTokens > 0) { hasPostCompactionUsage = true; + break; } - break; } } } diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 3394a063..31a0ec9c 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -12,7 +12,7 @@ import { type OAuthCredentials, type OAuthLoginCallbacks, type OAuthProviderId, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@earendil-works/pi-ai/oauth"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { dirname, join } from "path"; @@ -41,6 +41,10 @@ export type AuthStatus = { label?: string; }; +export interface GetApiKeyOptions { + includeFallback?: boolean; +} + type LockResult = { result: T; next?: string; @@ -199,7 +203,6 @@ export class InMemoryAuthStorageBackend implements AuthStorageBackend { export class AuthStorage { private data: AuthStorageData = {}; private runtimeOverrides: Map = new Map(); - private fallbackResolver?: (provider: string) => string | undefined; private loadError: Error | null = null; private errors: Error[] = []; private storage: AuthStorageBackend; @@ -238,14 +241,6 @@ export class AuthStorage { this.runtimeOverrides.delete(provider); } - /** - * Set a fallback resolver for API keys not found in auth.json or env vars. - * Used for custom provider keys from models.json. - */ - setFallbackResolver(resolver: (provider: string) => string | undefined): void { - this.fallbackResolver = resolver; - } - private recordError(error: unknown): void { const normalizedError = error instanceof Error ? error : new Error(String(error)); this.errors.push(normalizedError); @@ -350,7 +345,6 @@ export class AuthStorage { if (this.runtimeOverrides.has(provider)) return true; if (this.data[provider]) return true; if (getEnvApiKey(provider)) return true; - if (this.fallbackResolver?.(provider)) return true; return false; } @@ -371,10 +365,6 @@ export class AuthStorage { return { configured: false, source: "environment", label: envKeys[0] }; } - if (this.fallbackResolver?.(provider)) { - return { configured: false, source: "fallback", label: "custom provider config" }; - } - return { configured: false }; } @@ -468,9 +458,8 @@ export class AuthStorage { * 2. API key from auth.json * 3. OAuth token from auth.json (auto-refreshed with locking) * 4. Environment variable - * 5. Fallback resolver (models.json custom providers) */ - async getApiKey(providerId: string, options?: { includeFallback?: boolean }): Promise { + async getApiKey(providerId: string, options: GetApiKeyOptions = {}): Promise { // Runtime override takes highest priority const runtimeKey = this.runtimeOverrides.get(providerId); if (runtimeKey) { @@ -521,15 +510,12 @@ export class AuthStorage { } } + if (options.includeFallback === false) return undefined; + // Fall back to environment variable const envKey = getEnvApiKey(providerId); if (envKey) return envKey; - // Fall back to custom resolver (e.g., models.json custom providers) - if (options?.includeFallback !== false) { - return this.fallbackResolver?.(providerId) ?? undefined; - } - return undefined; } diff --git a/packages/coding-agent/src/core/compaction/branch-summarization.ts b/packages/coding-agent/src/core/compaction/branch-summarization.ts index 3378eb19..3f557c01 100644 --- a/packages/coding-agent/src/core/compaction/branch-summarization.ts +++ b/packages/coding-agent/src/core/compaction/branch-summarization.ts @@ -6,8 +6,8 @@ */ import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core"; -import type { Model, SimpleStreamOptions } from "@earendil-works/pi-ai"; -import { completeSimple } from "@earendil-works/pi-ai"; +import type { Model, SimpleStreamOptions } from "@earendil-works/pi-ai/compat"; +import { completeSimple } from "@earendil-works/pi-ai/compat"; import { convertToLlm, createBranchSummaryMessage, diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index 07f2f9fe..f369601c 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -6,8 +6,8 @@ */ import type { AgentMessage, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai"; -import { completeSimple } from "@earendil-works/pi-ai"; +import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai/compat"; +import { completeSimple } from "@earendil-works/pi-ai/compat"; import { convertToLlm, createBranchSummaryMessage, @@ -104,6 +104,7 @@ export interface CompactionResult { summary: string; firstKeptEntryId: string; tokensBefore: number; + estimatedTokensAfter?: number; /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */ details?: T; } @@ -138,12 +139,17 @@ export function calculateContextTokens(usage: Usage): number { /** * Get usage from an assistant message if available. - * Skips aborted and error messages as they don't have valid usage data. + * Skips aborted, error, and all-zero usage messages as they don't have valid usage data. */ 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; } } @@ -151,7 +157,7 @@ function getAssistantUsage(msg: AgentMessage): Usage | undefined { } /** - * Find the last non-aborted assistant message usage from session entries. + * Find the last valid assistant message usage from session entries. */ export function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined { for (let i = entries.length - 1; i >= 0; i--) { @@ -698,6 +704,10 @@ export function prepareCompaction( } } + if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) { + return undefined; + } + // Extract file operations from messages and previous compaction const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex); diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 081d2d11..a93f7c85 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -8,7 +8,7 @@ import { createRequire } from "node:module"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import * as _bundledPiAgentCore from "@earendil-works/pi-agent-core"; -import * as _bundledPiAi from "@earendil-works/pi-ai"; +import * as _bundledPiAiCompat from "@earendil-works/pi-ai/compat"; import * as _bundledPiAiOauth from "@earendil-works/pi-ai/oauth"; import type { KeyId } from "@earendil-works/pi-tui"; import * as _bundledPiTui from "@earendil-works/pi-tui"; @@ -50,12 +50,17 @@ const VIRTUAL_MODULES: Record = { "@sinclair/typebox/value": _bundledTypeboxValue, "@earendil-works/pi-agent-core": _bundledPiAgentCore, "@earendil-works/pi-tui": _bundledPiTui, - "@earendil-works/pi-ai": _bundledPiAi, + // Extensions resolve the pi-ai root to the compat entrypoint (a strict + // superset of the core entrypoint): existing extensions using the old + // global API keep working at runtime until compat is removed. + "@earendil-works/pi-ai": _bundledPiAiCompat, + "@earendil-works/pi-ai/compat": _bundledPiAiCompat, "@earendil-works/pi-ai/oauth": _bundledPiAiOauth, "@earendil-works/pi-coding-agent": _bundledPiCodingAgent, "@mariozechner/pi-agent-core": _bundledPiAgentCore, "@mariozechner/pi-tui": _bundledPiTui, - "@mariozechner/pi-ai": _bundledPiAi, + "@mariozechner/pi-ai": _bundledPiAiCompat, + "@mariozechner/pi-ai/compat": _bundledPiAiCompat, "@mariozechner/pi-ai/oauth": _bundledPiAiOauth, "@mariozechner/pi-coding-agent": _bundledPiCodingAgent, }; @@ -90,19 +95,24 @@ function getAliases(): Record { const piCodingAgentEntry = packageIndex; const piAgentCoreEntry = resolveWorkspaceOrImport("agent/dist/index.js", "@earendil-works/pi-agent-core"); const piTuiEntry = resolveWorkspaceOrImport("tui/dist/index.js", "@earendil-works/pi-tui"); - const piAiEntry = resolveWorkspaceOrImport("ai/dist/index.js", "@earendil-works/pi-ai"); + // Extensions resolve the pi-ai root to the compat entrypoint (a strict + // superset of the core entrypoint): existing extensions using the old + // global API keep working at runtime until compat is removed. + const piAiCompatEntry = resolveWorkspaceOrImport("ai/dist/compat.js", "@earendil-works/pi-ai/compat"); const piAiOauthEntry = resolveWorkspaceOrImport("ai/dist/oauth.js", "@earendil-works/pi-ai/oauth"); _aliases = { "@earendil-works/pi-coding-agent": piCodingAgentEntry, "@earendil-works/pi-agent-core": piAgentCoreEntry, "@earendil-works/pi-tui": piTuiEntry, - "@earendil-works/pi-ai": piAiEntry, + "@earendil-works/pi-ai": piAiCompatEntry, + "@earendil-works/pi-ai/compat": piAiCompatEntry, "@earendil-works/pi-ai/oauth": piAiOauthEntry, "@mariozechner/pi-coding-agent": piCodingAgentEntry, "@mariozechner/pi-agent-core": piAgentCoreEntry, "@mariozechner/pi-tui": piTuiEntry, - "@mariozechner/pi-ai": piAiEntry, + "@mariozechner/pi-ai": piAiCompatEntry, + "@mariozechner/pi-ai/compat": piAiCompatEntry, "@mariozechner/pi-ai/oauth": piAiOauthEntry, typebox: typeboxEntry, "typebox/compile": typeboxCompileEntry, @@ -117,6 +127,30 @@ function getAliases(): Record { type HandlerFn = (...args: unknown[]) => Promise; +let extensionCacheCwd: string | undefined; +let extensionCacheGeneration = 0; +const extensionCache = new Map(); + +interface ExtensionCacheToken { + cwd: string; + generation: number; +} + +export function clearExtensionCache(): void { + extensionCache.clear(); + extensionCacheCwd = undefined; + extensionCacheGeneration++; +} + +function useExtensionCacheCwd(cwd: string): ExtensionCacheToken { + const resolvedCwd = resolvePath(cwd); + if (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) { + clearExtensionCache(); + } + extensionCacheCwd = resolvedCwd; + return { cwd: resolvedCwd, generation: extensionCacheGeneration }; +} + /** * Create a runtime with throwing stubs for action methods. * Runner.bindCore() replaces these with real implementations. @@ -328,7 +362,22 @@ function createExtensionAPI( return api; } -async function loadExtensionModule(extensionPath: string) { +function isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken { + return ( + cacheToken !== undefined && + extensionCacheCwd === cacheToken.cwd && + extensionCacheGeneration === cacheToken.generation + ); +} + +async function loadExtensionModule(extensionPath: string, cacheToken?: ExtensionCacheToken) { + if (isCurrentCacheToken(cacheToken)) { + const cachedFactory = extensionCache.get(extensionPath); + if (cachedFactory) { + return cachedFactory; + } + } + const jiti = createJiti(import.meta.url, { moduleCache: false, // In Bun binary: use virtualModules for bundled packages (no filesystem resolution) @@ -339,7 +388,13 @@ async function loadExtensionModule(extensionPath: string) { const module = await jiti.import(extensionPath, { default: true }); const factory = module as ExtensionFactory; - return typeof factory !== "function" ? undefined : factory; + if (typeof factory !== "function") { + return undefined; + } + if (isCurrentCacheToken(cacheToken)) { + extensionCache.set(extensionPath, factory); + } + return factory; } /** @@ -370,11 +425,12 @@ async function loadExtension( cwd: string, eventBus: EventBus, runtime: ExtensionRuntime, + cacheToken?: ExtensionCacheToken, ): Promise<{ extension: Extension | null; error: string | null }> { const resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true }); try { - const factory = await loadExtensionModule(resolvedPath); + const factory = await loadExtensionModule(resolvedPath, cacheToken); if (!factory) { return { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` }; } @@ -410,20 +466,28 @@ export async function loadExtensionFromFactory( /** * Load extensions from paths. */ -export async function loadExtensions( +async function loadExtensionsInternal( paths: string[], cwd: string, eventBus?: EventBus, runtime?: ExtensionRuntime, + useCache = false, ): Promise { const extensions: Extension[] = []; const errors: Array<{ path: string; error: string }> = []; - const resolvedCwd = resolvePath(cwd); + const cacheToken = useCache ? useExtensionCacheCwd(cwd) : undefined; + const resolvedCwd = cacheToken?.cwd ?? resolvePath(cwd); const resolvedEventBus = eventBus ?? createEventBus(); const resolvedRuntime = runtime ?? createExtensionRuntime(); for (const extPath of paths) { - const { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, resolvedRuntime); + const { extension, error } = await loadExtension( + extPath, + resolvedCwd, + resolvedEventBus, + resolvedRuntime, + cacheToken, + ); if (error) { errors.push({ path: extPath, error }); @@ -442,6 +506,24 @@ export async function loadExtensions( }; } +export async function loadExtensions( + paths: string[], + cwd: string, + eventBus?: EventBus, + runtime?: ExtensionRuntime, +): Promise { + return loadExtensionsInternal(paths, cwd, eventBus, runtime); +} + +export async function loadExtensionsCached( + paths: string[], + cwd: string, + eventBus?: EventBus, + runtime?: ExtensionRuntime, +): Promise { + return loadExtensionsInternal(paths, cwd, eventBus, runtime, true); +} + interface PiManifest { extensions?: string[]; themes?: string[]; diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index a869a55d..7234d4e4 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -571,6 +571,10 @@ export interface SessionBeforeCompactEvent { preparation: CompactionPreparation; branchEntries: SessionEntry[]; customInstructions?: string; + /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */ + reason: "manual" | "threshold" | "overflow"; + /** True when the aborted turn is retried after this compaction (overflow recovery) */ + willRetry: boolean; signal: AbortSignal; } @@ -579,6 +583,10 @@ export interface SessionCompactEvent { type: "session_compact"; compactionEntry: CompactionEntry; fromExtension: boolean; + /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */ + reason: "manual" | "threshold" | "overflow"; + /** True when the aborted turn is retried after this compaction (overflow recovery) */ + willRetry: boolean; } /** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */ diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 60f9001f..70f39394 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -17,7 +17,7 @@ import { registerApiProvider, resetApiProviders, type SimpleStreamOptions, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { registerOAuthProvider, resetOAuthProviders } from "@earendil-works/pi-ai/oauth"; import { existsSync, readFileSync } from "fs"; import { join } from "path"; @@ -96,6 +96,13 @@ const ThinkingLevelMapSchema = Type.Object({ xhigh: Type.Optional(ThinkingLevelMapValueSchema), }); +const ChatTemplateKwargScalarSchema = Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()]); +const ChatTemplateKwargVariableSchema = Type.Object({ + $var: Type.Union([Type.Literal("thinking.enabled"), Type.Literal("thinking.effort")]), + omitWhenOff: Type.Optional(Type.Boolean()), +}); +const ChatTemplateKwargSchema = Type.Union([ChatTemplateKwargScalarSchema, ChatTemplateKwargVariableSchema]); + const OpenAICompletionsCompatSchema = Type.Object({ supportsStore: Type.Optional(Type.Boolean()), supportsDeveloperRole: Type.Optional(Type.Boolean()), @@ -114,9 +121,13 @@ const OpenAICompletionsCompatSchema = Type.Object({ Type.Literal("deepseek"), Type.Literal("zai"), Type.Literal("qwen"), + Type.Literal("chat-template"), Type.Literal("qwen-chat-template"), + Type.Literal("string-thinking"), + Type.Literal("ant-ling"), ]), ), + chatTemplateKwargs: Type.Optional(Type.Record(Type.String(), ChatTemplateKwargSchema)), cacheControlFormat: Type.Optional(Type.Literal("anthropic")), openRouterRouting: Type.Optional(OpenRouterRoutingSchema), vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema), @@ -289,6 +300,13 @@ function mergeCompat( }; } + if (baseCompletions?.chatTemplateKwargs || overrideCompletions.chatTemplateKwargs) { + mergedCompletions.chatTemplateKwargs = { + ...baseCompletions?.chatTemplateKwargs, + ...overrideCompletions.chatTemplateKwargs, + }; + } + return merged as Model["compat"]; } @@ -528,13 +546,11 @@ export class ModelRegistry { ); } } else if (!isBuiltIn) { - // Non-built-in providers with custom models require endpoint + auth. + // Non-built-in providers with custom models require an endpoint. + // Auth can come from auth.json, --api-key, or provider request config. if (!providerConfig.baseUrl) { throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`); } - if (!providerConfig.apiKey) { - throw new Error(`Provider ${providerName}: "apiKey" is required when defining custom models.`); - } } // Built-in providers with custom models: baseUrl/apiKey/api are optional, // inherited from built-in models. Auth comes from env vars / auth storage. @@ -783,7 +799,7 @@ export class ModelRegistry { * Get API key for a provider. */ async getApiKeyForProvider(provider: string): Promise { - const apiKey = await this.authStorage.getApiKey(provider, { includeFallback: false }); + const apiKey = await this.authStorage.getApiKey(provider); if (apiKey !== undefined) { return apiKey; } diff --git a/packages/coding-agent/src/core/provider-attribution.ts b/packages/coding-agent/src/core/provider-attribution.ts index 97ad1cfa..d3c6652d 100644 --- a/packages/coding-agent/src/core/provider-attribution.ts +++ b/packages/coding-agent/src/core/provider-attribution.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai"; import type { SettingsManager } from "./settings-manager.ts"; import { isInstallTelemetryEnabled } from "./telemetry.ts"; @@ -92,9 +92,9 @@ export function mergeProviderAttributionHeaders( model: Model, settingsManager: SettingsManager, sessionId: string | undefined, - ...headerSources: Array | undefined> -): Record | undefined { - const merged = { + ...headerSources: Array +): ProviderHeaders | undefined { + const merged: ProviderHeaders = { ...getSessionHeaders(model, sessionId), ...getDefaultAttributionHeaders(model, settingsManager), }; diff --git a/packages/coding-agent/src/core/provider-display-names.ts b/packages/coding-agent/src/core/provider-display-names.ts index 9b0371d2..d33c3d7d 100644 --- a/packages/coding-agent/src/core/provider-display-names.ts +++ b/packages/coding-agent/src/core/provider-display-names.ts @@ -26,7 +26,7 @@ export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record = { together: "Together AI", "vercel-ai-gateway": "Vercel AI Gateway", xai: "xAI", - zai: "ZAI", + zai: "ZAI Coding Plan (Global)", "zai-coding-cn": "ZAI Coding Plan (China)", xiaomi: "Xiaomi MiMo", "xiaomi-token-plan-cn": "Xiaomi MiMo Token Plan (China)", diff --git a/packages/coding-agent/src/core/resolve-config-value.ts b/packages/coding-agent/src/core/resolve-config-value.ts index 9d47cff3..6d75b001 100644 --- a/packages/coding-agent/src/core/resolve-config-value.ts +++ b/packages/coding-agent/src/core/resolve-config-value.ts @@ -152,11 +152,13 @@ export function resolveConfigValue(config: string, env?: Record) function executeWithConfiguredShell(command: string): { executed: boolean; value: string | undefined } { try { - const { shell, args } = getShellConfig(); - const result = spawnSync(shell, [...args, command], { + const { shell, args, commandTransport } = getShellConfig(); + const commandFromStdin = commandTransport === "stdin"; + const result = spawnSync(shell, commandFromStdin ? args : [...args, command], { encoding: "utf-8", + input: commandFromStdin ? command : undefined, timeout: 10000, - stdio: ["ignore", "pipe", "ignore"], + stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "ignore"], shell: false, windowsHide: true, }); diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index b35787af..18486ead 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -9,7 +9,12 @@ export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.ts"; import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.ts"; import { createEventBus, type EventBus } from "./event-bus.ts"; -import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.ts"; +import { + clearExtensionCache, + createExtensionRuntime, + loadExtensionFromFactory, + loadExtensionsCached, +} from "./extensions/loader.ts"; import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.ts"; import { DefaultPackageManager, type PathMetadata, type ResolvedResource } from "./package-manager.ts"; import type { PromptTemplate } from "./prompt-templates.ts"; @@ -206,6 +211,7 @@ export class DefaultResourceLoader implements ResourceLoader { private extensionThemeSourceInfos: Map; private lastPromptPaths: string[]; private lastThemePaths: string[]; + private loaded: boolean; constructor(options: DefaultResourceLoaderOptions) { this.cwd = resolvePath(options.cwd); @@ -252,6 +258,7 @@ export class DefaultResourceLoader implements ResourceLoader { this.extensionThemeSourceInfos = new Map(); this.lastPromptPaths = []; this.lastThemePaths = []; + this.loaded = false; } getExtensions(): LoadExtensionsResult { @@ -331,6 +338,10 @@ export class DefaultResourceLoader implements ResourceLoader { } async reload(options?: ResourceLoaderReloadOptions): Promise { + if (this.loaded) { + clearExtensionCache(); + } + let preTrustExtensions: LoadExtensionsResult | undefined; if (options?.resolveProjectTrust) { preTrustExtensions = await this.loadProjectTrustExtensions(); @@ -475,6 +486,7 @@ export class DefaultResourceLoader implements ResourceLoader { this.appendSystemPrompt = this.appendSystemPromptOverride ? this.appendSystemPromptOverride(baseAppend) : baseAppend; + this.loaded = true; } private async loadCurrentExtensionSet(options: { includeInlineFactories: boolean }): Promise { @@ -487,7 +499,7 @@ export class DefaultResourceLoader implements ResourceLoader { const extensionPaths = this.noExtensions ? cliEnabledExtensions : this.mergePaths(cliEnabledExtensions, enabledExtensions); - const extensionsResult = await loadExtensions(extensionPaths, this.cwd, this.eventBus); + const extensionsResult = await loadExtensionsCached(extensionPaths, this.cwd, this.eventBus); if (!options.includeInlineFactories) { return extensionsResult; } @@ -507,7 +519,7 @@ export class DefaultResourceLoader implements ResourceLoader { preTrustExtensions: LoadExtensionsResult | undefined, ): Promise { if (!preTrustExtensions) { - const extensionsResult = await loadExtensions(extensionPaths, this.cwd, this.eventBus); + const extensionsResult = await loadExtensionsCached(extensionPaths, this.cwd, this.eventBus); const inlineExtensions = await this.loadExtensionFactories(extensionsResult.runtime); extensionsResult.extensions.push(...inlineExtensions.extensions); extensionsResult.errors.push(...inlineExtensions.errors); @@ -527,7 +539,7 @@ export class DefaultResourceLoader implements ResourceLoader { const resolvedPath = this.resolveExtensionLoadPath(path); return !preloadedByPath.has(resolvedPath) && !failedPreloadPaths.has(resolvedPath); }); - const remainingExtensions = await loadExtensions( + const remainingExtensions = await loadExtensionsCached( remainingPaths, this.cwd, this.eventBus, diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 180846cb..3bec2c32 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -1,6 +1,6 @@ import { join } from "node:path"; import { Agent, type AgentMessage, type ThinkingLevel } from "@earendil-works/pi-agent-core"; -import { clampThinkingLevel, type Message, type Model, streamSimple } from "@earendil-works/pi-ai"; +import { clampThinkingLevel, type Message, type Model, streamSimple } from "@earendil-works/pi-ai/compat"; import { getAgentDir } from "../config.ts"; import { resolvePath } from "../utils/paths.ts"; import { AgentSession } from "./agent-session.ts"; diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 62942480..e40ed0c4 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -357,9 +357,10 @@ export function buildSessionContext( const path: SessionEntry[] = []; let current: SessionEntry | undefined = leaf; while (current) { - path.unshift(current); + path.push(current); current = current.parentId ? byId.get(current.parentId) : undefined; } + path.reverse(); // Extract settings and find compaction let thinkingLevel = "off"; @@ -1025,12 +1026,13 @@ export class SessionManager { /** Append a session info entry (e.g., display name). Returns entry id. */ appendSessionInfo(name: string): string { + const sanitizedName = name.replace(/[\r\n]+/g, " ").trim(); const entry: SessionInfoEntry = { type: "session_info", id: generateId(this.byId), parentId: this.leafId, timestamp: new Date().toISOString(), - name: name.trim(), + name: sanitizedName, }; this._appendEntry(entry); return entry.id; @@ -1152,9 +1154,10 @@ export class SessionManager { const startId = fromId ?? this.leafId; let current = startId ? this.byId.get(startId) : undefined; while (current) { - path.unshift(current); + path.push(current); current = current.parentId ? this.byId.get(current.parentId) : undefined; } + path.reverse(); return path; } diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 6b54a187..99ae71a4 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -334,11 +334,11 @@ export class SettingsManager { } /** Create an in-memory SettingsManager (no file I/O) */ - static inMemory(settings: Partial = {}): SettingsManager { + static inMemory(settings: Partial = {}, options: SettingsManagerCreateOptions = {}): SettingsManager { const storage = new InMemorySettingsStorage(); const initialSettings = SettingsManager.migrateSettings(structuredClone(settings) as Record); storage.withLock("global", () => JSON.stringify(initialSettings, null, 2)); - return SettingsManager.fromStorage(storage); + return SettingsManager.fromStorage(storage, options); } private static loadFromStorage(storage: SettingsStorage, scope: SettingsScope, projectTrusted = true): Settings { @@ -714,8 +714,15 @@ export class SettingsManager { this.save(); } + getThemeSetting(): string | undefined { + const value = this.settings.theme; + if (typeof value === "string") return value; + return undefined; + } + getTheme(): string | undefined { - return this.settings.theme; + const theme = this.getThemeSetting(); + return theme?.includes("/") ? undefined : theme; } setTheme(theme: string): void { diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index e56291bb..da6934e7 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -66,7 +66,7 @@ export interface BashOperations { export function createLocalBashOperations(options?: { shellPath?: string }): BashOperations { return { exec: async (command, cwd, { onData, signal, timeout, env }) => { - const { shell, args } = getShellConfig(options?.shellPath); + const shellConfig = getShellConfig(options?.shellPath); try { await fsAccess(cwd, constants.F_OK); } catch { @@ -76,13 +76,18 @@ export function createLocalBashOperations(options?: { shellPath?: string }): Bas throw new Error("aborted"); } - const child = spawn(shell, [...args, command], { + const commandFromStdin = shellConfig.commandTransport === "stdin"; + const child = spawn(shellConfig.shell, commandFromStdin ? shellConfig.args : [...shellConfig.args, command], { cwd, detached: process.platform !== "win32", env: env ?? getShellEnv(), - stdio: ["ignore", "pipe", "pipe"], + stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "pipe"], windowsHide: true, }); + if (commandFromStdin) { + child.stdin?.on("error", () => {}); + child.stdin?.end(command); + } if (child.pid) trackDetachedChildPid(child.pid); let timedOut = false; let timeoutHandle: NodeJS.Timeout | undefined; diff --git a/packages/coding-agent/src/core/tools/edit-diff.ts b/packages/coding-agent/src/core/tools/edit-diff.ts index f280bf19..5a4d966b 100644 --- a/packages/coding-agent/src/core/tools/edit-diff.ts +++ b/packages/coding-agent/src/core/tools/edit-diff.ts @@ -1,6 +1,5 @@ /** - * Shared diff computation utilities for the edit tool. - * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering). + * Shared diff computation utilities for the edit and similar tools. */ import * as Diff from "diff"; @@ -54,6 +53,124 @@ export function normalizeForFuzzyMatch(text: string): string { ); } +function splitLinesWithEndings(content: string): string[] { + return content.match(/[^\n]*\n|[^\n]+/g) ?? []; +} + +interface LineSpan { + start: number; + end: number; +} + +interface MatchedEdit { + editIndex: number; + matchIndex: number; + matchLength: number; + newText: string; +} + +type TextReplacement = Pick; + +function getLineSpans(content: string): LineSpan[] { + let offset = 0; + return splitLinesWithEndings(content).map((line) => { + const span = { start: offset, end: offset + line.length }; + offset = span.end; + return span; + }); +} + +function getReplacementLineRange(lines: LineSpan[], replacement: TextReplacement) { + const replacementStart = replacement.matchIndex; + const replacementEnd = replacement.matchIndex + replacement.matchLength; + + let startLine = -1; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (replacementStart >= line.start && replacementStart < line.end) { + startLine = i; + break; + } + } + if (startLine === -1) { + throw new Error("Replacement range is outside the base content."); + } + + let endLine = startLine; + while (endLine < lines.length && lines[endLine].end < replacementEnd) { + endLine++; + } + if (endLine >= lines.length) { + throw new Error("Replacement range is outside the base content."); + } + + return { startLine, endLine: endLine + 1 }; +} + +function applyReplacements(content: string, replacements: TextReplacement[], offset = 0): string { + let result = content; + for (let i = replacements.length - 1; i >= 0; i--) { + const replacement = replacements[i]; + const matchIndex = replacement.matchIndex - offset; + result = + result.substring(0, matchIndex) + replacement.newText + result.substring(matchIndex + replacement.matchLength); + } + return result; +} + +/** + * Apply replacements matched against `baseContent` to `originalContent` while + * preserving unchanged line blocks from the original. + * + * This is useful when `baseContent` is a normalized view of the original. Each + * replacement is widened to the lines it actually touches, those touched lines + * are rewritten from the normalized base, and all other lines are copied back + * from `originalContent`. The actual replacement ranges drive preservation so + * duplicate normalized lines cannot be aligned to the wrong occurrence. + */ +export function applyReplacementsPreservingUnchangedLines( + originalContent: string, + baseContent: string, + replacements: TextReplacement[], +): string { + const originalLines = splitLinesWithEndings(originalContent); + const baseLines = getLineSpans(baseContent); + if (originalLines.length !== baseLines.length) { + throw new Error("Cannot preserve unchanged lines because the base content has a different line count."); + } + + const groups: Array<{ startLine: number; endLine: number; replacements: TextReplacement[] }> = []; + const sortedReplacements = [...replacements].sort((a, b) => a.matchIndex - b.matchIndex); + for (const replacement of sortedReplacements) { + const range = getReplacementLineRange(baseLines, replacement); + const current = groups[groups.length - 1]; + if (current && range.startLine < current.endLine) { + current.endLine = Math.max(current.endLine, range.endLine); + current.replacements.push(replacement); + continue; + } + groups.push({ ...range, replacements: [replacement] }); + } + + let originalLineIndex = 0; + let result = ""; + for (const group of groups) { + result += originalLines.slice(originalLineIndex, group.startLine).join(""); + + const groupStartOffset = baseLines[group.startLine].start; + const groupEndOffset = baseLines[group.endLine - 1].end; + result += applyReplacements( + baseContent.slice(groupStartOffset, groupEndOffset), + group.replacements, + groupStartOffset, + ); + originalLineIndex = group.endLine; + } + result += originalLines.slice(originalLineIndex).join(""); + + return result; +} + export interface FuzzyMatchResult { /** Whether a match was found */ found: boolean; @@ -75,13 +192,6 @@ export interface Edit { newText: string; } -interface MatchedEdit { - editIndex: number; - matchIndex: number; - matchLength: number; - newText: string; -} - export interface AppliedEditsResult { baseContent: string; newContent: string; @@ -121,9 +231,9 @@ export function fuzzyFindText(content: string, oldText: string): FuzzyMatchResul }; } - // When fuzzy matching, we work in the normalized space for replacement. - // This means the output will have normalized whitespace/quotes/dashes, - // which is acceptable since we're fixing minor formatting differences anyway. + // When fuzzy matching, return offsets in normalized space. Callers can use + // the normalized content to compute replacements, then decide how much of + // that normalized output should be written back. return { found: true, index: fuzzyIndex, @@ -187,8 +297,9 @@ function getNoChangeError(path: string, totalEdits: number): Error { * * All edits are matched against the same original content. Replacements are * then applied in reverse order so offsets remain stable. If any edit needs - * fuzzy matching, the operation runs in fuzzy-normalized content space to - * preserve current single-edit behavior. + * fuzzy matching, the operation runs in fuzzy-normalized content space and then + * overlays those line-level changes onto the original content so unchanged line + * blocks keep their original bytes. */ export function applyEditsToNormalizedContent( normalizedContent: string, @@ -207,19 +318,18 @@ export function applyEditsToNormalizedContent( } const initialMatches = normalizedEdits.map((edit) => fuzzyFindText(normalizedContent, edit.oldText)); - const baseContent = initialMatches.some((match) => match.usedFuzzyMatch) - ? normalizeForFuzzyMatch(normalizedContent) - : normalizedContent; + const usedFuzzyMatch = initialMatches.some((match) => match.usedFuzzyMatch); + const replacementBaseContent = usedFuzzyMatch ? normalizeForFuzzyMatch(normalizedContent) : normalizedContent; const matchedEdits: MatchedEdit[] = []; for (let i = 0; i < normalizedEdits.length; i++) { const edit = normalizedEdits[i]; - const matchResult = fuzzyFindText(baseContent, edit.oldText); + const matchResult = fuzzyFindText(replacementBaseContent, edit.oldText); if (!matchResult.found) { throw getNotFoundError(path, i, normalizedEdits.length); } - const occurrences = countOccurrences(baseContent, edit.oldText); + const occurrences = countOccurrences(replacementBaseContent, edit.oldText); if (occurrences > 1) { throw getDuplicateError(path, i, normalizedEdits.length, occurrences); } @@ -243,14 +353,10 @@ export function applyEditsToNormalizedContent( } } - let newContent = baseContent; - for (let i = matchedEdits.length - 1; i >= 0; i--) { - const edit = matchedEdits[i]; - newContent = - newContent.substring(0, edit.matchIndex) + - edit.newText + - newContent.substring(edit.matchIndex + edit.matchLength); - } + const baseContent = normalizedContent; + const newContent = usedFuzzyMatch + ? applyReplacementsPreservingUnchangedLines(normalizedContent, replacementBaseContent, matchedEdits) + : applyReplacements(replacementBaseContent, matchedEdits); if (baseContent === newContent) { throw getNoChangeError(path, normalizedEdits.length); diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index 6f852f61..e03b728a 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -221,17 +221,24 @@ export function createFindToolDefinition( return; } - // Build fd arguments. --no-require-git makes fd apply hierarchical .gitignore - // semantics whether or not the search path is inside a git repository, without - // leaking sibling-directory rules the way --ignore-file (a global source) would. - const args: string[] = [ - "--glob", - "--color=never", - "--hidden", - "--no-require-git", - "--max-results", - String(effectiveLimit), - ]; + const args: string[] = ["--glob", "--color=never", "--hidden"]; + + // fd normally ignores .gitignore outside git repos, so keep --no-require-git + // there. Inside repos, use fd's default git-aware behavior so parent + // .gitignore rules stop at nested repo boundaries: + // https://github.com/earendil-works/pi/issues/5960 + let insideGitRepo = false; + for (let current = searchPath; ; ) { + if (await pathExists(path.join(current, ".git"))) { + insideGitRepo = true; + break; + } + const parent = path.dirname(current); + if (parent === current) break; + current = parent; + } + if (!insideGitRepo) args.push("--no-require-git"); + args.push("--max-results", String(effectiveLimit)); // fd --glob matches against the basename unless --full-path is set; in --full-path // mode it matches against the absolute candidate path, so a path-containing diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 70416af1..5830ecda 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -246,6 +246,7 @@ export { type SkillFrontmatter, } from "./core/skills.ts"; export { createSyntheticSourceInfo } from "./core/source-info.ts"; +export { type EditDiffResult, generateDiffString, generateUnifiedPatch } from "./core/tools/edit-diff.ts"; // Tools export { type BashOperations, diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index f66040bb..bbff87e7 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -49,6 +49,8 @@ import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.ts"; import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.ts"; +const EXTENSION_LOAD_FAILURE_HINT = 'Hint: Start without extensions using "pi -ne".'; + /** * Read all content from piped stdin. * Returns undefined if stdin is a TTY (interactive terminal). @@ -308,11 +310,11 @@ async function createSessionManager( } if (parsed.resume) { - initTheme(settingsManager.getTheme(), true); try { const selectedPath = await selectSession( (onProgress) => SessionManager.list(cwd, sessionDir, onProgress), (onProgress) => SessionManager.listAll(sessionDir, onProgress), + settingsManager, ); if (!selectedPath) { console.log(chalk.dim("No session selected")); @@ -774,6 +776,9 @@ export async function main(args: string[], options?: MainOptions) { time("resolveModelScope"); reportDiagnostics(runtime.diagnostics); if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) { + if (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes("Failed to load extension"))) { + console.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT)); + } process.exit(1); } time("createAgentSession"); @@ -805,9 +810,9 @@ export async function main(args: string[], options?: MainOptions) { if (startupBenchmark) { await interactiveMode.init(); time("interactiveMode.init"); - printTimings(); interactiveMode.stop(); stopThemeWatcher(); + printTimings(); if (process.stdout.writableLength > 0) { await new Promise((resolve) => process.stdout.once("drain", resolve)); } diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index a3cdf7d5..32711929 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -11,7 +11,7 @@ import { } from "@earendil-works/pi-tui"; import type { ModelRegistry } from "../../../core/model-registry.ts"; import type { SettingsManager } from "../../../core/settings-manager.ts"; -import { getModelSearchText } from "../model-search.ts"; +import { getModelSelectorSearchText } from "../model-search.ts"; import { theme } from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; import { keyHint } from "./keybinding-hints.ts"; @@ -219,7 +219,7 @@ export class ModelSelectorComponent extends Container implements Focusable { private filterModels(query: string): void { this.filteredModels = query ? fuzzyFilter(this.activeModels, query, ({ id, provider, model }) => - getModelSearchText({ id, provider, name: model.name }), + getModelSelectorSearchText({ id, provider, name: model.name }), ) : this.activeModels; this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1)); diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector.ts b/packages/coding-agent/src/modes/interactive/components/session-selector.ts index a92f0762..4949eee5 100644 --- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts @@ -190,6 +190,7 @@ class SessionSelectorHeader implements Component { interface SessionTreeNode { session: SessionInfo; children: SessionTreeNode[]; + latestActivity: number; } /** Flattened node for display with tree structure info */ @@ -210,7 +211,7 @@ function buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] { for (const session of sessions) { const sessionPath = canonicalizePath(session.path) ?? session.path; - byPath.set(sessionPath, { session, children: [] }); + byPath.set(sessionPath, { session, children: [], latestActivity: session.modified.getTime() }); } const roots: SessionTreeNode[] = []; @@ -227,9 +228,22 @@ function buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] { } } - // Sort children and roots by modified date (descending) + const updateLatestActivity = (node: SessionTreeNode): number => { + let latestActivity = node.session.modified.getTime(); + for (const child of node.children) { + latestActivity = Math.max(latestActivity, updateLatestActivity(child)); + } + node.latestActivity = latestActivity; + return latestActivity; + }; + + for (const root of roots) { + updateLatestActivity(root); + } + + // Sort children and roots by latest activity in each subtree (descending) const sortNodes = (nodes: SessionTreeNode[]): void => { - nodes.sort((a, b) => b.session.modified.getTime() - a.session.modified.getTime()); + nodes.sort((a, b) => b.latestActivity - a.latestActivity); for (const node of nodes) { sortNodes(node.children); } diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index 39d25f80..7cc92614 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -1,6 +1,7 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { Transport } from "@earendil-works/pi-ai"; import { + type Component, Container, getCapabilities, type SelectItem, @@ -13,7 +14,13 @@ import { } from "@earendil-works/pi-tui"; import { formatHttpIdleTimeoutMs, HTTP_IDLE_TIMEOUT_CHOICES } from "../../../core/http-dispatcher.ts"; import type { DefaultProjectTrust, WarningSettings } from "../../../core/settings-manager.ts"; -import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.ts"; +import { + getSelectListTheme, + getSettingsListTheme, + parseAutoThemeSetting, + type TerminalTheme, + theme, +} from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; import { keyDisplayText } from "./keybinding-hints.ts"; @@ -55,6 +62,7 @@ export interface SettingsConfig { thinkingLevel: ThinkingLevel; availableThinkingLevels: ThinkingLevel[]; currentTheme: string; + terminalTheme: TerminalTheme; availableThemes: string[]; hideThinkingBlock: boolean; collapseChangelog: boolean; @@ -210,6 +218,249 @@ class SelectSubmenu extends Container { } } +function themeItems(availableThemes: string[]): SelectItem[] { + return availableThemes.map((name) => ({ value: name, label: name })); +} + +const AUTOMATIC_THEME_VALUE = "/"; + +function singleModeThemeItems(availableThemes: string[]): SelectItem[] { + return [ + { + value: AUTOMATIC_THEME_VALUE, + label: "Automatic", + description: "Use separate themes for light and dark terminal appearance", + }, + ...themeItems(availableThemes), + ]; +} + +function preferredTheme(availableThemes: string[], preferred: string | undefined, fallback: string): string { + if (preferred && availableThemes.includes(preferred)) return preferred; + if (availableThemes.includes(fallback)) return fallback; + return availableThemes[0] ?? fallback; +} + +function defaultAutomaticThemes( + currentThemeSetting: string, + availableThemes: string[], +): { lightTheme: string; darkTheme: string } { + const autoTheme = parseAutoThemeSetting(currentThemeSetting); + if (autoTheme) return autoTheme; + + const currentFixedTheme = currentThemeSetting.includes("/") ? undefined : currentThemeSetting; + const themeName = preferredTheme(availableThemes, currentFixedTheme, "dark"); + return { lightTheme: themeName, darkTheme: themeName }; +} + +class ThemeSubmenu extends Container { + private inputComponent: Component | undefined; + private readonly callbacks: SettingsCallbacks; + private readonly availableThemes: string[]; + private readonly terminalTheme: TerminalTheme; + private readonly onDone: (selectedValue?: string) => void; + private readonly originalThemeSetting: string; + private mode: "single" | "automatic"; + private singleTheme: string; + private lightTheme: string; + private darkTheme: string; + + constructor( + currentThemeSetting: string, + terminalTheme: TerminalTheme, + availableThemes: string[], + callbacks: SettingsCallbacks, + onDone: (selectedValue?: string) => void, + ) { + super(); + this.callbacks = callbacks; + this.availableThemes = availableThemes; + this.terminalTheme = terminalTheme; + this.onDone = onDone; + this.originalThemeSetting = currentThemeSetting; + const autoTheme = parseAutoThemeSetting(currentThemeSetting); + const automaticThemes = defaultAutomaticThemes(currentThemeSetting, availableThemes); + const fixedTheme = autoTheme || currentThemeSetting.includes("/") ? undefined : currentThemeSetting; + this.mode = autoTheme ? "automatic" : "single"; + this.lightTheme = automaticThemes.lightTheme; + this.darkTheme = automaticThemes.darkTheme; + this.singleTheme = preferredTheme( + availableThemes, + fixedTheme ?? (autoTheme ? this.getActiveAutomaticTheme() : undefined), + "dark", + ); + + if (this.mode === "automatic") { + this.showAutomaticMenu(); + } else { + this.showSingleMenu(); + } + } + + handleInput(data: string): void { + this.inputComponent?.handleInput?.(data); + } + + private setContent(renderComponent: Component, inputComponent: Component = renderComponent): void { + this.clear(); + this.addChild(renderComponent); + this.inputComponent = inputComponent; + } + + private showSingleMenu(): void { + this.mode = "single"; + const menu = new SelectSubmenu( + "Theme", + "Select a theme, or choose Automatic to follow terminal appearance.", + singleModeThemeItems(this.availableThemes), + this.singleTheme, + (value) => { + if (value === AUTOMATIC_THEME_VALUE) { + this.mode = "automatic"; + this.callbacks.onThemePreview?.(this.getThemeSetting()); + this.showAutomaticMenu(); + return; + } + + this.singleTheme = value; + this.apply(value); + }, + () => this.cancel(), + (value) => { + this.callbacks.onThemePreview?.(value === AUTOMATIC_THEME_VALUE ? this.getAutomaticThemeSetting() : value); + }, + ); + this.setContent(menu); + } + + private showAutomaticMenu(): void { + this.mode = "automatic"; + const content = new Container(); + content.addChild(new Text(theme.bold(theme.fg("accent", "Automatic Theme")), 0, 0)); + content.addChild(new Spacer(1)); + content.addChild(new Text(theme.fg("muted", "Choose themes for terminal light and dark appearance."), 0, 0)); + content.addChild(new Text(theme.fg("muted", "Light/dark detection requires terminal support."), 0, 0)); + content.addChild(new Spacer(1)); + + const items: SettingItem[] = [ + { + id: "light-theme", + label: "Light theme", + description: "Theme to use in automatic mode when the terminal is light", + currentValue: this.lightTheme, + submenu: (currentValue, done) => + this.createThemeSelect( + "Light Theme", + "Select the theme to use for light terminal appearance", + currentValue, + done, + (value) => { + this.lightTheme = value; + this.callbacks.onThemePreview?.(this.getThemeSetting()); + done(value); + }, + ), + }, + { + id: "dark-theme", + label: "Dark theme", + description: "Theme to use in automatic mode when the terminal is dark", + currentValue: this.darkTheme, + submenu: (currentValue, done) => + this.createThemeSelect( + "Dark Theme", + "Select the theme to use for dark terminal appearance", + currentValue, + done, + (value) => { + this.darkTheme = value; + this.callbacks.onThemePreview?.(this.getThemeSetting()); + done(value); + }, + ), + }, + { + id: "apply", + label: "Apply", + description: "Save and go back", + currentValue: "save and go back", + values: ["save and go back"], + }, + { + id: "single-mode", + label: "Change mode", + description: "Switch to one theme for light and dark", + currentValue: "switch to single theme", + values: ["switch to single theme"], + }, + ]; + + const settingsList = new SettingsList( + items, + Math.min(items.length, 10), + getSettingsListTheme(), + (id) => { + switch (id) { + case "single-mode": + this.mode = "single"; + this.singleTheme = this.getActiveAutomaticTheme(); + this.callbacks.onThemePreview?.(this.singleTheme); + this.showSingleMenu(); + break; + case "apply": + this.apply(this.getAutomaticThemeSetting()); + break; + } + }, + () => this.cancel(), + ); + content.addChild(settingsList); + this.setContent(content, settingsList); + } + + private createThemeSelect( + title: string, + description: string, + currentValue: string, + done: (selectedValue?: string) => void, + onSelect: (value: string) => void, + ): SelectSubmenu { + return new SelectSubmenu( + title, + description, + themeItems(this.availableThemes), + currentValue, + onSelect, + () => { + this.callbacks.onThemePreview?.(this.getThemeSetting()); + done(); + }, + (value) => this.callbacks.onThemePreview?.(value), + ); + } + + private getThemeSetting(): string { + return this.mode === "automatic" ? this.getAutomaticThemeSetting() : this.singleTheme; + } + + private getActiveAutomaticTheme(): string { + return this.terminalTheme === "light" ? this.lightTheme : this.darkTheme; + } + + private getAutomaticThemeSetting(): string { + return `${this.lightTheme}/${this.darkTheme}`; + } + + private apply(themeSetting: string): void { + this.onDone(themeSetting); + } + + private cancel(): void { + this.callbacks.onThemePreview?.(this.originalThemeSetting); + this.onDone(); + } +} + /** * Main settings selector component. */ @@ -353,28 +604,7 @@ export class SettingsSelectorComponent extends Container { description: "Color theme for the interface", currentValue: config.currentTheme, submenu: (currentValue, done) => - new SelectSubmenu( - "Theme", - "Select color theme", - config.availableThemes.map((t) => ({ - value: t, - label: t, - })), - currentValue, - (value) => { - callbacks.onThemeChange(value); - done(value); - }, - () => { - // Restore original theme on cancel - callbacks.onThemePreview?.(currentValue); - done(); - }, - (value) => { - // Preview theme on selection change - callbacks.onThemePreview?.(value); - }, - ), + new ThemeSubmenu(currentValue, config.terminalTheme, config.availableThemes, callbacks, done), }, ]; @@ -561,6 +791,9 @@ export class SettingsSelectorComponent extends Container { case "terminal-progress": callbacks.onShowTerminalProgressChange(newValue === "true"); break; + case "theme": + callbacks.onThemeChange(newValue); + break; } }, callbacks.onCancel, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index ad9d3d3a..7b6dbbe8 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -16,7 +16,7 @@ import { type Model, type OAuthProviderId, type OAuthSelectPrompt, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import type { AutocompleteItem, AutocompleteProvider, @@ -128,22 +128,19 @@ import { UserMessageComponent } from "./components/user-message.ts"; import { UserMessageSelectorComponent } from "./components/user-message-selector.ts"; import { getModelSearchText } from "./model-search.ts"; import { - detectTerminalBackgroundTheme, getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, - initTheme, onThemeChange, setRegisteredThemes, - setTheme, - setThemeInstance, stopThemeWatcher, Theme, type ThemeColor, theme, } from "./theme/theme.ts"; +import { InteractiveThemeController } from "./theme/theme-controller.ts"; /** Interface for components that can be expanded/collapsed */ interface Expandable { @@ -268,6 +265,7 @@ export interface InteractiveModeOptions { export class InteractiveMode { private runtimeHost: AgentSessionRuntime; private ui: TUI; + private loadedResourcesContainer: Container; private chatContainer: Container; private pendingMessagesContainer: Container; private statusContainer: Container; @@ -374,6 +372,7 @@ export class InteractiveMode { private options: InteractiveModeOptions; private autoTrustOnReloadCwd: string | undefined; + private themeController: InteractiveThemeController; // Convenience accessors private get session(): AgentSession { @@ -397,12 +396,13 @@ export class InteractiveMode { this.resetExtensionUI(); }); this.runtimeHost.setRebindSession(async () => { - await this.rebindCurrentSession(); + await this.rebindCurrentSession({ renderBeforeBind: true }); }); this.version = VERSION; this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor()); this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink()); this.headerContainer = new Container(); + this.loadedResourcesContainer = new Container(); this.chatContainer = new Container(); this.pendingMessagesContainer = new Container(); this.statusContainer = new Container(); @@ -428,26 +428,12 @@ export class InteractiveMode { // Register themes from resource loader and initialize setRegisteredThemes(this.session.resourceLoader.getThemes().themes); - initTheme(this.settingsManager.getTheme(), true); - } - - private async detectThemeIfUnset(): Promise { - if (this.settingsManager.getTheme()) { - return; - } - - const detection = await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 }); - const result = setTheme(detection.theme, true); - if (!result.success) { - return; - } - - if (detection.confidence === "high") { - this.settingsManager.setTheme(detection.theme); - await this.settingsManager.flush(); - } - this.updateEditorBorderColor(); - this.ui.requestRender(); + this.themeController = new InteractiveThemeController( + this.ui, + this.settingsManager, + (message) => this.showError(message), + () => this.updateEditorBorderColor(), + ); } private getAutocompleteSourceTag(sourceInfo?: SourceInfo): string | undefined { @@ -652,8 +638,10 @@ export class InteractiveMode { console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`)); } - // Add header container as first child. Populate it after detectThemeIfUnset. + // Add header container as first child. Populate it after applying theme settings. + // Keep loaded resources before chat so restored session messages never precede them. this.ui.addChild(this.headerContainer); + this.ui.addChild(this.loadedResourcesContainer); this.ui.addChild(this.chatContainer); this.ui.addChild(this.pendingMessagesContainer); @@ -672,7 +660,7 @@ export class InteractiveMode { this.ui.start(); this.isInitialized = true; - await this.detectThemeIfUnset(); + await this.themeController.applyFromSettings(); // Add header with keybindings from config (unless silenced) if (this.options.verbose || !this.settingsManager.getQuietStartup()) { @@ -1346,6 +1334,9 @@ export class InteractiveMode { force?: boolean; showDiagnosticsWhenQuiet?: boolean; }): void { + // Resource rendering is idempotent; chat clears no longer clear this separate container. + this.loadedResourcesContainer.clear(); + const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup(); const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true; if (!showListing && !showDiagnostics) { @@ -1373,8 +1364,8 @@ export class InteractiveMode { 0, 0, ); - this.chatContainer.addChild(section); - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild(section); + this.loadedResourcesContainer.addChild(new Spacer(1)); }; const skillsResult = this.session.resourceLoader.getSkills(); @@ -1411,7 +1402,7 @@ export class InteractiveMode { if (showListing) { const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles; if (contextFiles.length > 0) { - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild(new Spacer(1)); const contextList = contextFiles .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`)) .join("\n"); @@ -1494,17 +1485,19 @@ export class InteractiveMode { const skillDiagnostics = skillsResult.diagnostics; if (skillDiagnostics.length > 0) { const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos); - this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild( + new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0), + ); + this.loadedResourcesContainer.addChild(new Spacer(1)); } const promptDiagnostics = promptsResult.diagnostics; if (promptDiagnostics.length > 0) { const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos); - this.chatContainer.addChild( + this.loadedResourcesContainer.addChild( new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0), ); - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild(new Spacer(1)); } const extensionDiagnostics: ResourceDiagnostic[] = []; @@ -1524,17 +1517,19 @@ export class InteractiveMode { if (extensionDiagnostics.length > 0) { const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos); - this.chatContainer.addChild( + this.loadedResourcesContainer.addChild( new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0), ); - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild(new Spacer(1)); } const themeDiagnostics = themesResult.diagnostics; if (themeDiagnostics.length > 0) { const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos); - this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); + this.loadedResourcesContainer.addChild( + new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0), + ); + this.loadedResourcesContainer.addChild(new Spacer(1)); } } } @@ -1559,12 +1554,7 @@ export class InteractiveMode { } this.statusContainer.clear(); try { - const result = await this.runtimeHost.newSession(options); - if (!result.cancelled) { - this.renderCurrentSessionState(); - this.ui.requestRender(); - } - return result; + return await this.runtimeHost.newSession(options); } catch (error: unknown) { return this.handleFatalRuntimeError("Failed to create session", error); } @@ -1573,7 +1563,6 @@ export class InteractiveMode { try { const result = await this.runtimeHost.fork(entryId, options); if (!result.cancelled) { - this.renderCurrentSessionState(); this.editor.setText(result.selectedText ?? ""); this.showStatus("Forked to new session"); } @@ -1647,12 +1636,18 @@ export class InteractiveMode { } } - private async rebindCurrentSession(): Promise { + private async rebindCurrentSession(options: { renderBeforeBind?: boolean } = {}): Promise { this.unsubscribe?.(); this.unsubscribe = undefined; this.applyRuntimeSettings(); - await this.bindCurrentSessionExtensions(); - this.subscribeToAgent(); + if (options.renderBeforeBind) { + this.renderCurrentSessionState(); + this.subscribeToAgent(); + await this.bindCurrentSessionExtensions(); + } else { + await this.bindCurrentSessionExtensions(); + this.subscribeToAgent(); + } await this.updateAvailableProviderCount(); this.updateEditorBorderColor(); this.updateTerminalTitle(); @@ -1667,6 +1662,7 @@ export class InteractiveMode { } private renderCurrentSessionState(): void { + this.loadedResourcesContainer.clear(); this.chatContainer.clear(); this.pendingMessagesContainer.clear(); this.compactionQueuedMessages = []; @@ -2080,16 +2076,13 @@ export class InteractiveMode { getTheme: (name) => getThemeByName(name), setTheme: (themeOrName) => { if (themeOrName instanceof Theme) { - setThemeInstance(themeOrName); - this.ui.requestRender(); - return { success: true }; + return this.themeController.setThemeInstance(themeOrName); } - const result = setTheme(themeOrName, true); + const result = this.themeController.setThemeName(themeOrName); if (result.success) { if (this.settingsManager.getTheme() !== themeOrName) { this.settingsManager.setTheme(themeOrName); } - this.ui.requestRender(); } return result; }, @@ -3378,6 +3371,7 @@ export class InteractiveMode { // which the stdout/stderr error handler turns into emergencyTerminalExit; // the render loop is already idle, so this cannot hot-spin (see #4144). await this.runtimeHost.dispose(); + this.themeController.disableAutoSync(); await this.ui.terminal.drainInput(1000); this.stop(); process.exit(0); @@ -3388,6 +3382,7 @@ export class InteractiveMode { // the final frame while the process is exiting. // Drain any in-flight Kitty key release events before stopping. // This prevents escape sequences from leaking to the parent shell over slow SSH. + this.themeController.disableAutoSync(); await this.ui.terminal.drainInput(1000); this.stop(); @@ -3623,9 +3618,11 @@ export class InteractiveMode { if (isExpandable(activeHeader)) { activeHeader.setExpanded(expanded); } - for (const child of this.chatContainer.children) { - if (isExpandable(child)) { - child.setExpanded(expanded); + for (const container of [this.loadedResourcesContainer, this.chatContainer]) { + for (const child of container.children) { + if (isExpandable(child)) { + child.setExpanded(expanded); + } } } this.ui.requestRender(); @@ -3717,7 +3714,6 @@ export class InteractiveMode { showError(errorMessage: string): void { this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0)); - this.chatContainer.addChild(new Spacer(1)); this.ui.requestRender(); } @@ -3732,7 +3728,7 @@ export class InteractiveMode { const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action; const changelogUrl = "https://pi.dev/changelog"; const changelogLink = getCapabilities().hyperlinks - ? hyperlink(theme.fg("accent", "open changelog"), changelogUrl) + ? hyperlink(theme.fg("accent", changelogUrl), changelogUrl) : theme.fg("accent", changelogUrl); const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink; const note = release.note?.trim(); @@ -3757,7 +3753,7 @@ export class InteractiveMode { } showPackageUpdateNotification(packages: string[]): void { - const action = theme.fg("accent", `${APP_NAME} update`); + const action = theme.fg("accent", `${APP_NAME} update --extensions`); const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action; const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n"); @@ -3991,7 +3987,8 @@ export class InteractiveMode { httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(), thinkingLevel: this.session.thinkingLevel, availableThinkingLevels: this.session.getAvailableThinkingLevels(), - currentTheme: this.settingsManager.getTheme() || "dark", + currentTheme: this.settingsManager.getThemeSetting() || "dark", + terminalTheme: this.themeController.getTerminalTheme(), availableThemes: getAvailableThemes(), hideThinkingBlock: this.hideThinkingBlock, collapseChangelog: this.settingsManager.getCollapseChangelog(), @@ -4058,21 +4055,11 @@ export class InteractiveMode { this.footer.invalidate(); this.updateEditorBorderColor(); }, - onThemeChange: (themeName) => { - const result = setTheme(themeName, true); - this.settingsManager.setTheme(themeName); - this.ui.invalidate(); - if (!result.success) { - this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`); - } - }, - onThemePreview: (themeName) => { - const result = setTheme(themeName, true); - if (result.success) { - this.ui.invalidate(); - this.ui.requestRender(); - } + onThemeChange: (themeSetting) => { + this.settingsManager.setTheme(themeSetting); + void this.themeController.applyFromSettings(); }, + onThemePreview: (themeName) => this.themeController.preview(themeName), onHideThinkingBlockChange: (hidden) => { this.hideThinkingBlock = hidden; this.settingsManager.setHideThinkingBlock(hidden); @@ -4403,7 +4390,6 @@ export class InteractiveMode { return; } - this.renderCurrentSessionState(); this.editor.setText(result.selectedText ?? ""); done(); this.showStatus("Forked to new session"); @@ -4436,7 +4422,6 @@ export class InteractiveMode { return; } - this.renderCurrentSessionState(); this.editor.setText(""); this.showStatus("Cloned to new session"); } catch (error: unknown) { @@ -4628,7 +4613,6 @@ export class InteractiveMode { if (result.cancelled) { return result; } - this.renderCurrentSessionState(); this.showStatus("Resumed session"); return result; } catch (error: unknown) { @@ -4646,7 +4630,6 @@ export class InteractiveMode { if (result.cancelled) { return result; } - this.renderCurrentSessionState(); this.showStatus("Resumed session in current cwd"); return result; } @@ -5099,8 +5082,20 @@ export class InteractiveMode { this.ui.requestRender(); }; + let chatRestoredBeforeSessionStart = false; + let reloadBoxDismissed = false; + const restoreChatBeforeSessionStart = () => { + if (chatRestoredBeforeSessionStart) { + return; + } + this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); + this.rebuildChatFromMessages(); + chatRestoredBeforeSessionStart = true; + }; + try { - await this.session.reload(); + await this.session.reload({ beforeSessionStart: restoreChatBeforeSessionStart }); + restoreChatBeforeSessionStart(); configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs()); this.keybindings.reload(); const activeHeader = this.customHeader ?? this.builtInHeader; @@ -5108,12 +5103,7 @@ export class InteractiveMode { activeHeader.setExpanded(this.toolOutputExpanded); } setRegisteredThemes(this.session.resourceLoader.getThemes().themes); - this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); - const themeName = this.settingsManager.getTheme(); - const themeResult = themeName ? setTheme(themeName, true) : { success: true }; - if (!themeResult.success) { - this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`); - } + await this.themeController.applyFromSettings(); const editorPaddingX = this.settingsManager.getEditorPaddingX(); const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible(); this.defaultEditor.setPaddingX(editorPaddingX); @@ -5127,8 +5117,6 @@ export class InteractiveMode { this.setupAutocompleteProvider(); const runner = this.session.extensionRunner; this.setupExtensionShortcuts(runner); - this.rebuildChatFromMessages(); - dismissReloadBox(this.editor as Component); this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true, @@ -5143,8 +5131,12 @@ export class InteractiveMode { ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust" : "Reloaded keybindings, extensions, skills, prompts, themes", ); + dismissReloadBox(this.editor as Component); + reloadBoxDismissed = true; } catch (error) { - dismissReloadBox(previousEditor as Component); + if (!reloadBoxDismissed) { + dismissReloadBox(previousEditor as Component); + } this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`); } } @@ -5218,7 +5210,6 @@ export class InteractiveMode { this.showStatus("Import cancelled"); return; } - this.renderCurrentSessionState(); this.showStatus(`Session imported from: ${inputPath}`); } catch (error: unknown) { if (error instanceof MissingSessionCwdError) { @@ -5232,7 +5223,6 @@ export class InteractiveMode { this.showStatus("Import cancelled"); return; } - this.renderCurrentSessionState(); this.showStatus(`Session imported from: ${inputPath}`); return; } @@ -5368,8 +5358,12 @@ export class InteractiveMode { } this.session.setSessionName(name); + const sessionName = this.sessionManager.getSessionName(); + if (sessionName !== name) { + this.showWarning(`Session name was normalized from ${JSON.stringify(name)} to ${JSON.stringify(sessionName)}`); + } this.chatContainer.addChild(new Spacer(1)); - this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0)); + this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${sessionName ?? name}`), 1, 0)); this.ui.requestRender(); } @@ -5571,7 +5565,6 @@ export class InteractiveMode { if (result.cancelled) { return; } - this.renderCurrentSessionState(); this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1)); this.ui.requestRender(); @@ -5725,14 +5718,6 @@ export class InteractiveMode { } private async handleCompactCommand(customInstructions?: string): Promise { - const entries = this.sessionManager.getEntries(); - const messageCount = entries.filter((e) => e.type === "message").length; - - if (messageCount < 2) { - this.showWarning("Nothing to compact (no messages yet)"); - return; - } - if (this.loadingAnimation) { this.loadingAnimation.stop(); this.loadingAnimation = undefined; @@ -5754,6 +5739,7 @@ export class InteractiveMode { this.loadingAnimation.stop(); this.loadingAnimation = undefined; } + this.themeController.disableAutoSync(); this.clearExtensionTerminalInputListeners(); this.footer.dispose(); this.footerDataProvider.dispose(); diff --git a/packages/coding-agent/src/modes/interactive/model-search.ts b/packages/coding-agent/src/modes/interactive/model-search.ts index f1dbc73c..bab9c5a5 100644 --- a/packages/coding-agent/src/modes/interactive/model-search.ts +++ b/packages/coding-agent/src/modes/interactive/model-search.ts @@ -9,3 +9,13 @@ export function getModelSearchText(item: ModelSearchItem): string { const name = item.name ? ` ${item.name}` : ""; return `${id} ${provider} ${provider}/${id} ${provider} ${id}${name}`; } + +/** + * The /model selector search should rank exact provider-prefixed queries before proxy-provider IDs + * like openrouter/openai/gpt-5, so keep the bare model ID out of the leading position. + */ +export function getModelSelectorSearchText(item: ModelSearchItem): string { + const { id, provider } = item; + const name = item.name ? ` ${item.name}` : ""; + return `${provider} ${provider}/${id} ${provider} ${id}${name}`; +} diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts new file mode 100644 index 00000000..43ad620d --- /dev/null +++ b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts @@ -0,0 +1,126 @@ +import type { TUI } from "@earendil-works/pi-tui"; +import type { SettingsManager } from "../../../core/settings-manager.ts"; +import { + detectTerminalBackgroundFromEnv, + detectTerminalBackgroundTheme, + detectTerminalThemeForAuto, + initTheme, + parseAutoThemeSetting, + resolveThemeSetting, + setTheme, + setThemeInstance, + type TerminalTheme, + type Theme, +} from "./theme.ts"; + +type ThemeResult = { success: boolean; error?: string }; + +export class InteractiveThemeController { + private readonly ui: TUI; + private readonly settingsManager: SettingsManager; + private readonly showError: (message: string) => void; + private readonly onChanged: () => void; + private terminalTheme: TerminalTheme = detectTerminalBackgroundFromEnv().theme; + private activeThemeName: string | undefined; + private autoSyncEnabled = false; + + constructor(ui: TUI, settingsManager: SettingsManager, showError: (message: string) => void, onChanged: () => void) { + this.ui = ui; + this.settingsManager = settingsManager; + this.showError = showError; + this.onChanged = onChanged; + this.activeThemeName = resolveThemeSetting(this.settingsManager.getThemeSetting(), this.terminalTheme); + initTheme(this.activeThemeName, true); + this.ui.onTerminalColorSchemeChange((terminalTheme) => this.applyTerminalTheme(terminalTheme)); + } + + async applyFromSettings(): Promise { + const themeSetting = this.settingsManager.getThemeSetting(); + const autoTheme = parseAutoThemeSetting(themeSetting); + if (autoTheme) { + this.terminalTheme = await detectTerminalThemeForAuto({ ui: this.ui, timeoutMs: 100 }); + this.setAutoSync(true); + this.applyThemeName(this.terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme, true); + return; + } + + this.setAutoSync(false); + if (themeSetting !== undefined) { + this.applyThemeName(themeSetting, true); + return; + } + + const detection = await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 }); + this.terminalTheme = detection.theme; + if (!this.applyThemeName(detection.theme).success) return; + if (detection.confidence === "high") { + this.settingsManager.setTheme(detection.theme); + await this.settingsManager.flush(); + } + } + + setThemeName(themeName: string, showError = false): ThemeResult { + this.setAutoSync(false); + return this.applyThemeName(themeName, showError); + } + + setThemeInstance(themeInstance: Theme): ThemeResult { + this.setAutoSync(false); + setThemeInstance(themeInstance); + this.activeThemeName = ""; + this.notifyChanged(); + return { success: true }; + } + + preview(themeSettingOrName: string): void { + const themeName = resolveThemeSetting(themeSettingOrName, this.terminalTheme) ?? this.activeThemeName; + if (!themeName) return; + if (setTheme(themeName, true).success) { + this.ui.invalidate(); + this.ui.requestRender(); + } + } + + disableAutoSync(): void { + this.setAutoSync(false); + } + + getTerminalTheme(): TerminalTheme { + return this.terminalTheme; + } + + private applyThemeName(themeName: string, showError = false): ThemeResult { + const result = setTheme(themeName, true); + this.activeThemeName = result.success ? themeName : "dark"; + this.notifyChanged(); + if (!result.success && showError) { + this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`); + } + return result; + } + + private notifyChanged(): void { + this.ui.invalidate(); + this.onChanged(); + } + + private setAutoSync(enabled: boolean): void { + if (this.autoSyncEnabled === enabled) return; + this.autoSyncEnabled = enabled; + this.ui.setTerminalColorSchemeNotifications(enabled); + } + + private applyTerminalTheme(terminalTheme: TerminalTheme): void { + if (!this.autoSyncEnabled) return; + this.terminalTheme = terminalTheme; + const autoTheme = parseAutoThemeSetting(this.settingsManager.getThemeSetting()); + if (!autoTheme) { + this.setAutoSync(false); + return; + } + const themeName = terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme; + if (themeName !== this.activeThemeName) { + this.applyThemeName(themeName); + } + } +} diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json index 7bc495da..9d94a12a 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json +++ b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json @@ -11,7 +11,8 @@ }, "name": { "type": "string", - "description": "Theme name" + "pattern": "^[^/]+$", + "description": "Theme name. Must not contain '/' because it is reserved for automatic light/dark theme settings." }, "vars": { "type": "object", diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 778f8028..676bc529 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -503,6 +503,14 @@ function getCustomThemeInfos(): ThemeInfo[] { return result; } +function assertThemeNameIsValid(name: string): void { + if (name.includes("/")) { + throw new Error( + `Invalid theme name "${name}": theme names cannot contain "/" because it is reserved for automatic light/dark theme settings.`, + ); + } +} + function parseThemeJson(label: string, json: unknown): ThemeJson { if (!validateThemeJson.Check(json)) { const errors = Array.from(validateThemeJson.Errors(json)); @@ -539,7 +547,9 @@ function parseThemeJson(label: string, json: unknown): ThemeJson { throw new Error(errorMessage); } - return json as ThemeJson; + const themeJson = json as ThemeJson; + assertThemeNameIsValid(themeJson.name); + return themeJson; } function parseThemeJsonContent(label: string, content: string): ThemeJson { @@ -625,6 +635,36 @@ export function getThemeByName(name: string): Theme | undefined { export type TerminalTheme = "dark" | "light"; +export function parseAutoThemeSetting( + themeSetting: string | undefined, +): { lightTheme: string; darkTheme: string } | undefined { + if (!themeSetting) return undefined; + const slashIndex = themeSetting.indexOf("/"); + if (slashIndex === -1 || themeSetting.indexOf("/", slashIndex + 1) !== -1) { + return undefined; + } + + const lightTheme = themeSetting.slice(0, slashIndex).trim(); + const darkTheme = themeSetting.slice(slashIndex + 1).trim(); + if (!lightTheme || !darkTheme) { + return undefined; + } + return { lightTheme, darkTheme }; +} + +export function resolveThemeSetting( + themeSetting: string | undefined, + terminalTheme: TerminalTheme, +): string | undefined { + const autoTheme = parseAutoThemeSetting(themeSetting); + if (autoTheme) { + return terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme; + } + if (themeSetting?.includes("/")) return undefined; + if (typeof themeSetting === "string") return themeSetting; + return undefined; +} + export interface TerminalThemeDetection { theme: TerminalTheme; source: "terminal background" | "COLORFGBG" | "fallback"; @@ -640,11 +680,20 @@ export interface TerminalBackgroundThemeDetector { queryTerminalBackgroundColor({ timeoutMs }: { timeoutMs: number }): Promise; } +export interface TerminalAutoThemeDetector extends TerminalBackgroundThemeDetector { + queryTerminalColorScheme?({ timeoutMs }: { timeoutMs: number }): Promise; +} + export interface TerminalBackgroundThemeDetectionOptions extends TerminalThemeDetectionOptions { ui: TerminalBackgroundThemeDetector; timeoutMs: number; } +export interface TerminalAutoThemeDetectionOptions extends TerminalThemeDetectionOptions { + ui: TerminalAutoThemeDetector; + timeoutMs: number; +} + function getColorFgBgBackgroundIndex(colorfgbg: string): number | undefined { const parts = colorfgbg.split(";"); for (let i = parts.length - 1; i >= 0; i--) { @@ -715,6 +764,20 @@ export async function detectTerminalBackgroundTheme({ return detectTerminalBackgroundFromEnv({ env }); } +export async function detectTerminalThemeForAuto({ + ui, + timeoutMs, + env, +}: TerminalAutoThemeDetectionOptions): Promise { + try { + const colorScheme = await ui.queryTerminalColorScheme?.({ timeoutMs }); + if (colorScheme) return colorScheme; + } catch { + // Fall back to OSC 11 / COLORFGBG detection when color-scheme DSR is unsupported. + } + return (await detectTerminalBackgroundTheme({ ui, timeoutMs, env })).theme; +} + export function getDefaultTheme(): string { return detectTerminalBackgroundFromEnv().theme; } @@ -752,6 +815,7 @@ export function setRegisteredThemes(themes: Theme[]): void { registeredThemes.clear(); for (const theme of themes) { if (theme.name) { + assertThemeNameIsValid(theme.name); registeredThemes.set(theme.name, theme); } } diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index e00de1b4..32980851 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -12,6 +12,7 @@ import { getSelfUpdateUnavailableInstruction, PACKAGE_NAME, type SelfUpdateCommand, + type SelfUpdatePackageTarget, VERSION, } from "./config.ts"; import type { ExtensionFactory } from "./core/extensions/types.ts"; @@ -52,6 +53,7 @@ interface PackageCommandOptions { command: PackageCommand; source?: string; updateTarget?: UpdateTarget; + showExtensionsSkippedNote: boolean; local: boolean; force: boolean; projectTrustOverride?: boolean; @@ -79,7 +81,7 @@ function getPackageCommandUsage(command: PackageCommand): string { case "remove": return `${APP_NAME} remove [-l] [--approve|--no-approve]`; case "update": - return `${APP_NAME} update [source|self|pi] [--self] [--extensions] [--extension ] [--approve|--no-approve] [--force]`; + return `${APP_NAME} update [source|self|pi] [--self|--extensions|--all] [--extension ] [--approve|--no-approve] [--force]`; case "list": return `${APP_NAME} list [--approve|--no-approve]`; } @@ -133,15 +135,17 @@ Examples: Update pi and installed packages. Options: - --self Update pi only + --self Update pi only (default when no target is given) --extensions Update installed packages only + --all Update pi and installed packages --extension Update one package only -a, --approve Trust project-local files for this command -na, --no-approve Ignore project-local files for this command --force Reinstall pi even if the current version is latest Short forms: - ${APP_NAME} update Update pi and all extensions + ${APP_NAME} update Update pi only + ${APP_NAME} update --all Update pi and all extensions ${APP_NAME} update Update one package ${APP_NAME} update pi Update pi only (self works as alias to pi) `); @@ -184,6 +188,7 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined let source: string | undefined; let selfFlag = false; let extensionsFlag = false; + let allFlag = false; let extensionFlagSource: string | undefined; for (let index = 0; index < rest.length; index++) { @@ -220,6 +225,15 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined continue; } + if (arg === "--all") { + if (command === "update") { + allFlag = true; + } else { + invalidOption = invalidOption ?? arg; + } + continue; + } + if (arg === "--approve" || arg === "-a") { projectTrustOverride = true; continue; @@ -271,10 +285,20 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined } let updateTarget: UpdateTarget | undefined; + let showExtensionsSkippedNote = false; if (command === "update") { + if (allFlag && (selfFlag || extensionsFlag || extensionFlagSource)) { + conflictingOptions = + conflictingOptions ?? "--all cannot be combined with --self, --extensions, or --extension"; + } + if (allFlag && source) { + conflictingOptions = conflictingOptions ?? "--all cannot be combined with a positional source"; + } + if (extensionFlagSource) { - if (selfFlag || extensionsFlag) { - conflictingOptions = conflictingOptions ?? "--extension cannot be combined with --self or --extensions"; + if (selfFlag || extensionsFlag || allFlag) { + conflictingOptions = + conflictingOptions ?? "--extension cannot be combined with --self, --extensions, or --all"; } if (source) { conflictingOptions = conflictingOptions ?? "--extension cannot be combined with a positional source"; @@ -285,12 +309,15 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined if (sourceIsSelf) { updateTarget = extensionsFlag ? { type: "all" } : { type: "self" }; } else { - if (extensionsFlag || selfFlag) { + if (extensionsFlag || selfFlag || allFlag) { conflictingOptions = - conflictingOptions ?? "positional update targets cannot be combined with --self or --extensions"; + conflictingOptions ?? + "positional update targets cannot be combined with --self, --extensions, or --all"; } updateTarget = { type: "extensions", source }; } + } else if (allFlag) { + updateTarget = { type: "all" }; } else if (selfFlag && extensionsFlag) { updateTarget = { type: "all" }; } else if (selfFlag) { @@ -298,7 +325,8 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined } else if (extensionsFlag) { updateTarget = { type: "extensions" }; } else { - updateTarget = { type: "all" }; + updateTarget = { type: "self" }; + showExtensionsSkippedNote = true; } } @@ -306,6 +334,7 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined command, source, updateTarget, + showExtensionsSkippedNote, local, force, projectTrustOverride, @@ -325,9 +354,12 @@ function updateTargetIncludesExtensions(target: UpdateTarget): boolean { return target.type === "all" || target.type === "extensions"; } -function printSelfUpdateUnavailable(npmCommand?: string[], updatePackageName = PACKAGE_NAME): void { +function printSelfUpdateUnavailable( + npmCommand?: string[], + updatePackageTarget: SelfUpdatePackageTarget = PACKAGE_NAME, +): void { console.error(`error: ${APP_NAME} cannot self-update this installation.`); - console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageName)); + console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageTarget)); const entrypoint = process.argv[1]; if (entrypoint) { @@ -362,27 +394,38 @@ function printSelfUpdateNote(note: string): void { interface SelfUpdatePlan { packageName: string; + installSpec: string; + version: string; shouldRun: boolean; note?: string; } async function getSelfUpdatePlan(force: boolean): Promise { - if (force) { - return { packageName: PACKAGE_NAME, shouldRun: true }; + let latestRelease: Awaited>; + try { + latestRelease = await getLatestPiRelease(VERSION); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Could not determine latest ${APP_NAME} version: ${message}`); + } + if (!latestRelease) { + throw new Error(`Could not determine latest ${APP_NAME} version.`); } - try { - const latestRelease = await getLatestPiRelease(VERSION); - const packageName = latestRelease?.packageName ?? PACKAGE_NAME; - if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) { - return { packageName, shouldRun: true, ...(latestRelease?.note ? { note: latestRelease.note } : {}) }; - } - } catch { - return { packageName: PACKAGE_NAME, shouldRun: true }; + const packageName = latestRelease.packageName ?? PACKAGE_NAME; + const installSpec = `${packageName}@${latestRelease.version}`; + if (force || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) { + return { + packageName, + installSpec, + version: latestRelease.version, + ...(latestRelease.note ? { note: latestRelease.note } : {}), + shouldRun: true, + }; } console.log(chalk.green(`${APP_NAME} is already up to date (v${VERSION})`)); - return { packageName: PACKAGE_NAME, shouldRun: false }; + return { packageName, installSpec, version: latestRelease.version, shouldRun: false }; } async function runSelfUpdate(command: SelfUpdateCommand): Promise { @@ -660,7 +703,12 @@ export async function handlePackageCommand( } case "update": { - const target = options.updateTarget ?? { type: "all" }; + const target = options.updateTarget ?? { type: "self" }; + if (options.showExtensionsSkippedNote) { + console.log( + chalk.dim(`Extensions are skipped. Run ${APP_NAME} update --extensions to update extensions.`), + ); + } if (updateTargetIncludesExtensions(target)) { const updateSource = target.type === "extensions" ? target.source : undefined; await packageManager.update(updateSource); @@ -684,13 +732,13 @@ export async function handlePackageCommand( process.exitCode = 1; return true; } - const selfUpdateCommand = getSelfUpdateCommand( - PACKAGE_NAME, - selfUpdateNpmCommand, - selfUpdatePlan.packageName, - ); + const selfUpdateTarget = { + packageName: selfUpdatePlan.packageName, + installSpec: selfUpdatePlan.installSpec, + }; + const selfUpdateCommand = getSelfUpdateCommand(PACKAGE_NAME, selfUpdateNpmCommand, selfUpdateTarget); if (!selfUpdateCommand) { - printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdatePlan.packageName); + printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdateTarget); process.exitCode = 1; return true; } @@ -709,7 +757,7 @@ export async function handlePackageCommand( process.exitCode = 1; return true; } - console.log(chalk.green(`Updated ${APP_NAME}`)); + console.log(chalk.green(`Updated ${APP_NAME} from ${VERSION} to ${selfUpdatePlan.version}`)); } return true; } diff --git a/packages/coding-agent/src/utils/shell.ts b/packages/coding-agent/src/utils/shell.ts index 817ba4f9..2cafa595 100644 --- a/packages/coding-agent/src/utils/shell.ts +++ b/packages/coding-agent/src/utils/shell.ts @@ -6,11 +6,21 @@ import { getBinDir } from "../config.ts"; export interface ShellConfig { shell: string; args: string[]; + commandTransport?: "argv" | "stdin"; } /** * Find bash executable on PATH (cross-platform) */ +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"] }; +} + function findBashOnPath(): string | null { if (process.platform === "win32") { // Windows: Use 'where' and verify file exists (where can return non-existent paths) @@ -58,7 +68,7 @@ export function getShellConfig(customShellPath?: string): ShellConfig { // 1. Check user-specified shell path if (customShellPath) { if (existsSync(customShellPath)) { - return { shell: customShellPath, args: ["-c"] }; + return getBashShellConfig(customShellPath); } throw new Error(`Custom shell path not found: ${customShellPath}`); } @@ -77,14 +87,14 @@ export function getShellConfig(customShellPath?: string): ShellConfig { for (const path of paths) { if (existsSync(path)) { - return { shell: path, args: ["-c"] }; + return getBashShellConfig(path); } } // 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.) const bashOnPath = findBashOnPath(); if (bashOnPath) { - return { shell: bashOnPath, args: ["-c"] }; + return getBashShellConfig(bashOnPath); } throw new Error( @@ -98,12 +108,12 @@ export function getShellConfig(customShellPath?: string): ShellConfig { // Unix: try /bin/bash, then bash on PATH, then fallback to sh if (existsSync("/bin/bash")) { - return { shell: "/bin/bash", args: ["-c"] }; + return getBashShellConfig("/bin/bash"); } const bashOnPath = findBashOnPath(); if (bashOnPath) { - return { shell: bashOnPath, args: ["-c"] }; + return getBashShellConfig(bashOnPath); } return { shell: "sh", args: ["-c"] }; diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index 1abe3e28..6384b5a0 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -2,7 +2,8 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { type AssistantMessage, getModel } from "@earendil-works/pi-ai"; +import { type AssistantMessage, createAssistantMessageEventStream, fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; @@ -11,51 +12,10 @@ import { SessionManager } from "../src/core/session-manager.ts"; import { SettingsManager } from "../src/core/settings-manager.ts"; import { createTestResourceLoader } from "./utilities.ts"; -vi.mock("../src/core/compaction/index.js", () => ({ - calculateContextTokens: (usage: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - totalTokens?: number; - }) => usage.totalTokens ?? usage.input + usage.output + usage.cacheRead + usage.cacheWrite, - collectEntriesForBranchSummary: () => ({ entries: [], commonAncestorId: null }), - compact: async () => ({ - summary: "compacted", - firstKeptEntryId: "entry-1", - tokensBefore: 100, - details: {}, - }), - estimateContextTokens: ( - messages: Array<{ - role: string; - usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens?: number }; - stopReason?: string; - }>, - ) => { - // Walk backwards to find last non-error, non-aborted assistant with usage - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg.role === "assistant" && msg.stopReason !== "error" && msg.stopReason !== "aborted" && msg.usage) { - const tokens = - msg.usage.totalTokens ?? msg.usage.input + msg.usage.output + msg.usage.cacheRead + msg.usage.cacheWrite; - return { tokens, usageTokens: tokens, trailingTokens: 0, lastUsageIndex: i }; - } - } - return { tokens: 0, usageTokens: 0, trailingTokens: 0, lastUsageIndex: null }; - }, - generateBranchSummary: async () => ({ summary: "", aborted: false, readFiles: [], modifiedFiles: [] }), - prepareCompaction: () => ({ dummy: true }), - shouldCompact: ( - contextTokens: number, - contextWindow: number, - settings: { enabled: boolean; reserveTokens: number }, - ) => settings.enabled && contextTokens > contextWindow - settings.reserveTokens, -})); - describe("AgentSession auto-compaction queue resume", () => { let session: AgentSession; let sessionManager: SessionManager; + let settingsManager: SettingsManager; let tempDir: string; beforeEach(() => { @@ -73,7 +33,7 @@ describe("AgentSession auto-compaction queue resume", () => { }); sessionManager = SessionManager.inMemory(); - const settingsManager = SettingsManager.create(tempDir, tempDir); + settingsManager = SettingsManager.create(tempDir, tempDir); const authStorage = AuthStorage.create(join(tempDir, "auth.json")); authStorage.setRuntimeApiKey("anthropic", "test-key"); const modelRegistry = ModelRegistry.create(authStorage, tempDir); @@ -98,6 +58,57 @@ describe("AgentSession auto-compaction queue resume", () => { }); it("should resume after threshold compaction when only agent-level queued messages exist", async () => { + settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); + const model = session.model!; + const now = Date.now(); + sessionManager.appendMessage({ + role: "user", + content: [{ type: "text", text: "message to compact" }], + timestamp: now - 1000, + }); + sessionManager.appendMessage({ + role: "assistant", + content: [{ type: "text", text: "assistant response to compact" }], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 100, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 100, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: now - 500, + }); + session.agent.state.messages = sessionManager.buildSessionContext().messages; + session.agent.streamFn = (summaryModel) => { + const stream = createAssistantMessageEventStream(); + queueMicrotask(() => { + stream.push({ + type: "done", + reason: "stop", + message: { + ...fauxAssistantMessage("compacted"), + api: summaryModel.api, + provider: summaryModel.provider, + model: summaryModel.id, + usage: { + input: 10, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 10, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + }, + }); + }); + return stream; + }; + session.agent.followUp({ role: "custom", customType: "test", diff --git a/packages/coding-agent/test/agent-session-branching.test.ts b/packages/coding-agent/test/agent-session-branching.test.ts index f516e6d6..76d355e1 100644 --- a/packages/coding-agent/test/agent-session-branching.test.ts +++ b/packages/coding-agent/test/agent-session-branching.test.ts @@ -10,7 +10,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { AgentSession } from "../src/core/agent-session.ts"; import { diff --git a/packages/coding-agent/test/agent-session-compaction.test.ts b/packages/coding-agent/test/agent-session-compaction.test.ts index 5724d684..516c3b45 100644 --- a/packages/coding-agent/test/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/agent-session-compaction.test.ts @@ -11,7 +11,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; diff --git a/packages/coding-agent/test/agent-session-concurrent.test.ts b/packages/coding-agent/test/agent-session-concurrent.test.ts index dcd2ac9b..46599068 100644 --- a/packages/coding-agent/test/agent-session-concurrent.test.ts +++ b/packages/coding-agent/test/agent-session-concurrent.test.ts @@ -13,7 +13,7 @@ import { getModel, type ImageContent, type TextContent, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; diff --git a/packages/coding-agent/test/agent-session-dynamic-provider.test.ts b/packages/coding-agent/test/agent-session-dynamic-provider.test.ts index a6da8c9e..eee5583d 100644 --- a/packages/coding-agent/test/agent-session-dynamic-provider.test.ts +++ b/packages/coding-agent/test/agent-session-dynamic-provider.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; import { DefaultResourceLoader } from "../src/core/resource-loader.ts"; diff --git a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts index cf64a17f..88871ac8 100644 --- a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts +++ b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { DefaultResourceLoader } from "../src/core/resource-loader.ts"; diff --git a/packages/coding-agent/test/agent-session-retry.test.ts b/packages/coding-agent/test/agent-session-retry.test.ts index ebba143b..6e3d0582 100644 --- a/packages/coding-agent/test/agent-session-retry.test.ts +++ b/packages/coding-agent/test/agent-session-retry.test.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent, type AgentEvent, type AgentTool } from "@earendil-works/pi-agent-core"; -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 { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; diff --git a/packages/coding-agent/test/agent-session-runtime-events.test.ts b/packages/coding-agent/test/agent-session-runtime-events.test.ts index 42b01fe1..348c0451 100644 --- a/packages/coding-agent/test/agent-session-runtime-events.test.ts +++ b/packages/coding-agent/test/agent-session-runtime-events.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; +import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import { type CreateAgentSessionRuntimeFactory, diff --git a/packages/coding-agent/test/agent-session-stats.test.ts b/packages/coding-agent/test/agent-session-stats.test.ts index b435246e..7e2a5f3f 100644 --- a/packages/coding-agent/test/agent-session-stats.test.ts +++ b/packages/coding-agent/test/agent-session-stats.test.ts @@ -1,5 +1,5 @@ import { Agent } from "@earendil-works/pi-agent-core"; -import { type AssistantMessage, getModel, type Usage } from "@earendil-works/pi-ai"; +import { type AssistantMessage, getModel, type Usage } from "@earendil-works/pi-ai/compat"; import { describe, expect, it } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; @@ -140,4 +140,28 @@ describe("AgentSession.getSessionStats", () => { session.dispose(); } }); + + it("ignores zero-usage messages when checking for post-compaction context usage", () => { + const { session, sessionManager } = createSession(); + + try { + sessionManager.appendMessage(createUserMessage("first", 1)); + sessionManager.appendMessage(createAssistantMessage("response1", 180_000, 2)); + const keptUserId = sessionManager.appendMessage(createUserMessage("second", 3)); + sessionManager.appendMessage(createAssistantMessage("response2", 195_000, 4)); + sessionManager.appendCompaction("summary", keptUserId, 195_000); + sessionManager.appendMessage(createUserMessage("third", 5)); + sessionManager.appendMessage(createAssistantMessage("response3", 25_000, 6)); + sessionManager.appendMessage(createUserMessage("continue", 7)); + sessionManager.appendMessage(createAssistantMessage("partial", 0, 8)); + syncAgentMessages(session, sessionManager); + + const stats = session.getSessionStats(); + expect(stats.contextUsage).toBeDefined(); + expect(stats.contextUsage?.tokens).not.toBeNull(); + expect(stats.contextUsage?.tokens ?? 0).toBeGreaterThan(25_000); + } finally { + session.dispose(); + } + }); }); diff --git a/packages/coding-agent/test/auth-storage.test.ts b/packages/coding-agent/test/auth-storage.test.ts index bcc353c1..651f9a17 100644 --- a/packages/coding-agent/test/auth-storage.test.ts +++ b/packages/coding-agent/test/auth-storage.test.ts @@ -5,7 +5,8 @@ import { registerOAuthProvider } from "@earendil-works/pi-ai/oauth"; import lockfile from "proper-lockfile"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; -import { clearConfigValueCache } from "../src/core/resolve-config-value.ts"; +import { clearConfigValueCache, resolveConfigValueUncached } from "../src/core/resolve-config-value.ts"; +import * as shellModule from "../src/utils/shell.ts"; describe("AuthStorage", () => { let tempDir: string; @@ -321,6 +322,30 @@ describe("AuthStorage", () => { expect(apiKey).toBe("hello-world"); }); + test("command config uses stdin when configured shell requires it", () => { + if (process.platform === "win32") return; + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + vi.spyOn(shellModule, "getShellConfig").mockReturnValue({ + shell: "/bin/bash", + args: ["-s"], + commandTransport: "stdin", + }); + + try { + Object.defineProperty(process, "platform", { + configurable: true, + value: "win32", + }); + const nameExpansion = "$" + "{name}"; + + expect(resolveConfigValueUncached(`!name='World'; echo "Hello, ${nameExpansion}!"`)).toBe("Hello, World!"); + } finally { + if (platformDescriptor) { + Object.defineProperty(process, "platform", platformDescriptor); + } + } + }); + describe("caching", () => { test("command is only executed once per process", async () => { // Use a command that writes to a file to count invocations diff --git a/packages/coding-agent/test/compaction-extensions.test.ts b/packages/coding-agent/test/compaction-extensions.test.ts index 2c41210e..62ca9780 100644 --- a/packages/coding-agent/test/compaction-extensions.test.ts +++ b/packages/coding-agent/test/compaction-extensions.test.ts @@ -6,7 +6,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; @@ -98,6 +98,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => { const sessionManager = SessionManager.create(tempDir); const settingsManager = SettingsManager.create(tempDir, tempDir); + settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); const authStorage = AuthStorage.create(join(tempDir, "auth.json")); const modelRegistry = ModelRegistry.create(authStorage); diff --git a/packages/coding-agent/test/compaction-summary-reasoning.test.ts b/packages/coding-agent/test/compaction-summary-reasoning.test.ts index 6609aca1..306a3b1c 100644 --- a/packages/coding-agent/test/compaction-summary-reasoning.test.ts +++ b/packages/coding-agent/test/compaction-summary-reasoning.test.ts @@ -7,8 +7,8 @@ const { completeSimpleMock } = vi.hoisted(() => ({ completeSimpleMock: vi.fn(), })); -vi.mock("@earendil-works/pi-ai", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("@earendil-works/pi-ai/compat", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, completeSimple: completeSimpleMock, diff --git a/packages/coding-agent/test/compaction.test.ts b/packages/coding-agent/test/compaction.test.ts index 929d06ec..ab09c3bf 100644 --- a/packages/coding-agent/test/compaction.test.ts +++ b/packages/coding-agent/test/compaction.test.ts @@ -1,6 +1,6 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, Usage } from "@earendil-works/pi-ai"; -import { getModel } from "@earendil-works/pi-ai"; +import type { AssistantMessage, Usage } from "@earendil-works/pi-ai/compat"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { readFileSync } from "fs"; import { join } from "path"; import { beforeEach, describe, expect, it } from "vitest"; @@ -218,12 +218,43 @@ describe("getLastAssistantUsage", () => { expect(usage!.input).toBe(100); }); + it("should skip all-zero assistant usage", () => { + const entries: SessionEntry[] = [ + createMessageEntry(createUserMessage("Hello")), + createMessageEntry(createAssistantMessage("Hi", createMockUsage(100, 50))), + createMessageEntry(createUserMessage("continue")), + createMessageEntry(createAssistantMessage("Partial", createMockUsage(0, 0))), + ]; + + const usage = getLastAssistantUsage(entries); + expect(usage).not.toBeNull(); + expect(usage!.input).toBe(100); + }); + it("should return undefined if no assistant messages", () => { const entries: SessionEntry[] = [createMessageEntry(createUserMessage("Hello"))]; expect(getLastAssistantUsage(entries)).toBeUndefined(); }); }); +describe("estimateContextTokens", () => { + it("uses the last non-zero assistant usage as the context anchor", () => { + const messages: AgentMessage[] = [ + createUserMessage("Hello"), + createAssistantMessage("Hi", createMockUsage(100, 50)), + createUserMessage("continue"), + createAssistantMessage("Partial thinking", createMockUsage(0, 0)), + ]; + + const estimate = estimateContextTokens(messages); + + expect(estimate.usageTokens).toBe(150); + expect(estimate.lastUsageIndex).toBe(1); + expect(estimate.trailingTokens).toBeGreaterThan(0); + expect(estimate.tokens).toBe(150 + estimate.trailingTokens); + }); +}); + describe("shouldCompact", () => { it("should return true when context exceeds threshold", () => { const settings: CompactionSettings = { @@ -396,7 +427,7 @@ describe("buildSessionContext", () => { }); describe("prepareCompaction with previous compaction", () => { - it("should preserve kept messages across repeated compactions when they still fit", () => { + it("should skip repeated compactions when kept messages still fit", () => { const u1 = createMessageEntry(createUserMessage("user msg 1 (summarized by compaction1)")); const a1 = createMessageEntry(createAssistantMessage("assistant msg 1")); const u2 = createMessageEntry(createUserMessage("user msg 2 - kept by compaction1")); @@ -408,29 +439,9 @@ describe("prepareCompaction with previous compaction", () => { const a4 = createMessageEntry(createAssistantMessage("assistant msg 4", createMockUsage(8000, 2000))); const pathEntries = [u1, a1, u2, a2, u3, a3, compaction1, u4, a4]; - const contextBefore = buildSessionContext(pathEntries); const preparation = prepareCompaction(pathEntries, DEFAULT_COMPACTION_SETTINGS); - expect(preparation).toBeDefined(); - expect(preparation!.firstKeptEntryId).toBe(u2.id); - expect(preparation!.previousSummary).toBe("First summary"); - expect(extractText(preparation!.messagesToSummarize)).not.toContain("First summary"); - expect(preparation!.tokensBefore).toBe(estimateContextTokens(contextBefore.messages).tokens); - - const compaction2: CompactionEntry = { - type: "compaction", - id: "compaction2-id", - parentId: a4.id, - timestamp: new Date().toISOString(), - summary: "Second summary", - firstKeptEntryId: preparation!.firstKeptEntryId, - tokensBefore: preparation!.tokensBefore, - }; - const contextAfter = buildSessionContext([...pathEntries, compaction2]); - const contextAfterText = extractText(contextAfter.messages); - - expect(contextAfterText).toContain("user msg 2 - kept by compaction1"); - expect(contextAfterText).toContain("user msg 3 - kept by compaction1"); + expect(preparation).toBeUndefined(); }); it("should re-summarize previously kept messages when the recent window moves past them", () => { diff --git a/packages/coding-agent/test/config.test.ts b/packages/coding-agent/test/config.test.ts index cbfa044e..49448cf1 100644 --- a/packages/coding-agent/test/config.test.ts +++ b/packages/coding-agent/test/config.test.ts @@ -188,6 +188,29 @@ describe("detectInstallMethod", () => { }); }); + test("self-updates exact npm versions without uninstalling the current package", () => { + const { prefix } = createNpmPrefixInstall(); + + const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent", undefined, { + packageName: "@earendil-works/pi-coding-agent", + installSpec: "@earendil-works/pi-coding-agent@1.2.3", + }); + + expect(command).toEqual({ + command: "npm", + args: [ + "--prefix", + prefix, + "install", + "-g", + "--ignore-scripts", + "--min-release-age=0", + "@earendil-works/pi-coding-agent@1.2.3", + ], + display: `npm --prefix ${prefix} install -g --ignore-scripts --min-release-age=0 @earendil-works/pi-coding-agent@1.2.3`, + }); + }); + test("self-updates renamed packages from the current install prefix", () => { const { prefix } = createNpmPrefixInstall(); diff --git a/packages/coding-agent/test/interactive-mode-clone-command.test.ts b/packages/coding-agent/test/interactive-mode-clone-command.test.ts index fea69c5d..69864146 100644 --- a/packages/coding-agent/test/interactive-mode-clone-command.test.ts +++ b/packages/coding-agent/test/interactive-mode-clone-command.test.ts @@ -41,7 +41,7 @@ describe("InteractiveMode /clone", () => { await interactiveModePrototype.handleCloneCommand.call(context); expect(fork).toHaveBeenCalledWith("leaf-123", { position: "at" }); - expect(renderCurrentSessionState).toHaveBeenCalled(); + expect(renderCurrentSessionState).not.toHaveBeenCalled(); expect(setText).toHaveBeenCalledWith(""); expect(showStatus).toHaveBeenCalledWith("Cloned to new session"); expect(showError).not.toHaveBeenCalled(); diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 8f1018b2..7d75d383 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -119,11 +119,13 @@ describe("InteractiveMode.showStatus", () => { describe("InteractiveMode.setToolsExpanded", () => { test("applies expansion state to the active header and chat entries", () => { const header = { setExpanded: vi.fn() }; + const loadedResourcesChild = { setExpanded: vi.fn() }; const chatChild = { setExpanded: vi.fn() }; const fakeThis: any = { toolOutputExpanded: false, customHeader: undefined, builtInHeader: header, + loadedResourcesContainer: { children: [loadedResourcesChild] }, chatContainer: { children: [chatChild] }, ui: { requestRender: vi.fn() }, }; @@ -132,6 +134,7 @@ describe("InteractiveMode.setToolsExpanded", () => { expect(fakeThis.toolOutputExpanded).toBe(true); expect(header.setExpanded).toHaveBeenCalledWith(true); + expect(loadedResourcesChild.setExpanded).toHaveBeenCalledWith(true); expect(chatChild.setExpanded).toHaveBeenCalledWith(true); expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1); }); @@ -151,6 +154,13 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const fakeThis: any = { session: { settingsManager }, settingsManager, + themeController: { + setThemeInstance: vi.fn(() => ({ success: true })), + setThemeName: vi.fn(() => { + fakeThis.ui.requestRender(); + return { success: true }; + }), + }, ui: { requestRender: vi.fn() }, }; @@ -158,6 +168,7 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const result = uiContext.setTheme("light"); expect(result.success).toBe(true); + expect(fakeThis.themeController.setThemeName).toHaveBeenCalledWith("light"); expect(settingsManager.setTheme).toHaveBeenCalledWith("light"); expect(currentTheme).toBe("light"); expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1); @@ -173,6 +184,10 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const fakeThis: any = { session: { settingsManager }, settingsManager, + themeController: { + setThemeInstance: vi.fn(() => ({ success: true })), + setThemeName: vi.fn(() => ({ success: false, error: "Theme not found" })), + }, ui: { requestRender: vi.fn() }, }; @@ -180,6 +195,7 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const result = uiContext.setTheme("__missing_theme__"); expect(result.success).toBe(false); + expect(fakeThis.themeController.setThemeName).toHaveBeenCalledWith("__missing_theme__"); expect(settingsManager.setTheme).not.toHaveBeenCalled(); expect(fakeThis.ui.requestRender).not.toHaveBeenCalled(); }); @@ -428,6 +444,7 @@ describe("InteractiveMode.showLoadedResources", () => { const fakeThis: any = { options: { verbose: options.verbose ?? false }, toolOutputExpanded: options.toolOutputExpanded ?? false, + loadedResourcesContainer: new Container(), chatContainer: new Container(), settingsManager: { getQuietStartup: () => options.quietStartup, @@ -606,7 +623,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer); + const output = renderAll(fakeThis.loadedResourcesContainer); expect(output).toContain("[Skills]"); expect(output).toContain("commit"); expect(output).not.toContain("resource-list"); @@ -623,7 +640,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer); + const output = renderAll(fakeThis.loadedResourcesContainer); expect(output).toContain("[Skills]"); expect(output).toContain("resource-list"); expect(output).not.toContain("commit"); @@ -641,7 +658,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer); + const output = renderAll(fakeThis.loadedResourcesContainer); expect(output).toContain("[Skills]"); expect(output).toContain("resource-list"); expect(output).not.toContain("commit"); @@ -657,7 +674,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer); + const output = renderAll(fakeThis.loadedResourcesContainer); expect(output).toContain("[Extensions]"); expect(output).toContain("answer.ts, btw.ts"); expect(output).not.toContain("extensions/answer.ts"); @@ -674,7 +691,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] @scope/pi-scoped, answer.ts, cli-extension.ts, HazAT/pi-interactive-subagents, HazAT/pi-interactive-subagents:subagents, local-index, pi-markdown-preview, user-index"`); }); @@ -720,7 +737,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] alpha/one, beta/one, gamma/one"`); }); @@ -748,7 +765,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] plan-mode"`); }); @@ -776,7 +793,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] plan-mode"`); }); @@ -813,7 +830,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] plan-mode, webfetch.ts"`); }); @@ -850,7 +867,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] bar, foo"`); }); @@ -887,7 +904,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] alpha/tools, beta/tools"`); }); @@ -915,7 +932,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] main.ts"`); }); @@ -943,7 +960,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] pi-markdown-preview"`); }); @@ -959,7 +976,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` + expect(normalizeRenderedOutput(fakeThis.loadedResourcesContainer)).toMatchInlineSnapshot(` "[Extensions] project /tmp/project/.pi/extensions/answer.ts @@ -990,7 +1007,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/"); + const output = renderAll(fakeThis.loadedResourcesContainer).replace(/\\/g, "/"); expect(output).toContain("[Context]"); expect(output).toContain("~/.pi/agent/AGENTS.md, AGENTS.md"); expect(output).not.toContain(`${cwd.replace(/\\/g, "/")}/AGENTS.md`); @@ -1010,7 +1027,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: false, }); - const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/"); + const output = renderAll(fakeThis.loadedResourcesContainer).replace(/\\/g, "/"); expect(output).toContain("[Context]"); expect(output).toContain("~/.pi/agent/AGENTS.md"); expect(output).toContain("~/Development/pi-mono/AGENTS.md"); @@ -1029,7 +1046,7 @@ describe("InteractiveMode.showLoadedResources", () => { showDiagnosticsWhenQuiet: true, }); - expect(fakeThis.chatContainer.children).toHaveLength(0); + expect(fakeThis.loadedResourcesContainer.children).toHaveLength(0); }); test("still shows diagnostics on quiet startup when requested", () => { @@ -1044,7 +1061,7 @@ describe("InteractiveMode.showLoadedResources", () => { showDiagnosticsWhenQuiet: true, }); - const output = renderAll(fakeThis.chatContainer); + const output = renderAll(fakeThis.loadedResourcesContainer); expect(output).toContain("[Skill conflicts]"); expect(output).not.toContain("[Skills]"); }); diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index fe8609f2..fb559e1d 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -1,8 +1,14 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { AnthropicMessagesCompat, Api, Context, Model, OpenAICompletionsCompat } from "@earendil-works/pi-ai"; -import { getApiProvider } from "@earendil-works/pi-ai"; +import type { + AnthropicMessagesCompat, + Api, + Context, + Model, + OpenAICompletionsCompat, +} from "@earendil-works/pi-ai/compat"; +import { getApiProvider } from "@earendil-works/pi-ai/compat"; import { getOAuthProvider } from "@earendil-works/pi-ai/oauth"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; @@ -240,9 +246,10 @@ describe("ModelRegistry", () => { expect(model?.baseUrl).toBe("https://openrouter.ai/api/v1"); }); - test("non-built-in provider custom models still require baseUrl and apiKey", () => { + test("non-built-in provider custom models still require baseUrl", () => { writeRawModelsJson({ "my-custom-provider": { + apiKey: "test-key", models: [ { id: "my-model", @@ -434,6 +441,43 @@ describe("ModelRegistry", () => { expect(compat?.cacheControlFormat).toBe("anthropic"); }); + test("compat schema accepts chat template thinking configuration", () => { + writeRawModelsJson({ + demo: { + baseUrl: "https://example.com/v1", + apiKey: "DEMO_KEY", + api: "openai-completions", + models: [ + { + id: "demo-model", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1000, + maxTokens: 100, + compat: { + thinkingFormat: "chat-template", + chatTemplateKwargs: { + preserve_thinking: true, + thinking: { $var: "thinking.enabled" }, + }, + }, + }, + ], + }, + }); + + const registry = ModelRegistry.create(authStorage, modelsJsonPath); + const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined; + + expect(registry.getError()).toBeUndefined(); + expect(compat?.thinkingFormat).toBe("chat-template"); + expect(compat?.chatTemplateKwargs).toEqual({ + preserve_thinking: true, + thinking: { $var: "thinking.enabled" }, + }); + }); + test("compat schema accepts Anthropic eager tool input streaming flag", () => { writeRawModelsJson({ demo: { @@ -848,6 +892,7 @@ describe("ModelRegistry", () => { expect(registry.getProviderDisplayName("openai")).toBe("OpenAI"); expect(registry.getProviderDisplayName("github-copilot")).toBe("GitHub Copilot"); + expect(registry.getProviderDisplayName("zai")).toBe("ZAI Coding Plan (Global)"); expect(registry.getProviderDisplayName("unknown-provider")).toBe("unknown-provider"); registry.registerProvider("named-provider", { @@ -1669,6 +1714,25 @@ describe("ModelRegistry", () => { expect(count).toBe(0); }); + test("getAvailable filters GitHub Copilot OAuth models to account picker availability", () => { + authStorage.set("github-copilot", { + type: "oauth", + refresh: "github-access-token", + access: "tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;", + expires: Date.now() + 60_000, + availableModelIds: ["gpt-4.1"], + }); + + const registry = ModelRegistry.create(authStorage, modelsJsonPath); + + expect( + registry + .getAvailable() + .filter((m) => m.provider === "github-copilot") + .map((m) => m.id), + ).toEqual(["gpt-4.1"]); + }); + test("getApiKeyAndHeaders resolves authHeader on every request", async () => { const tokenFile = join(tempDir, "token"); writeFileSync(tokenFile, "token-1"); diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index df87cf34..6df65ded 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -374,7 +374,7 @@ describe("package commands", () => { } }); - it("uses global npmCommand and current package name for forced self updates without checking the api", async () => { + it("uses the update check version for forced self updates even when current", async () => { const globalPrefix = join(tempDir, "global-prefix"); const projectPrefix = join(tempDir, "project-prefix"); const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@earendil-works", "pi-coding-agent"); @@ -402,7 +402,7 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); - const fetchMock = vi.fn(); + const fetchMock = vi.fn(async () => Response.json({ version: VERSION })); vi.stubGlobal("fetch", fetchMock); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); @@ -413,11 +413,14 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); - expect(fetchMock).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledOnce(); + const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; expect(recordedArgs).toContain(globalPrefix); - expect(recordedArgs).toContain(PACKAGE_NAME); + expect(recordedArgs).toContain(`${PACKAGE_NAME}@${VERSION}`); + expect(recordedArgs).not.toContain(PACKAGE_NAME); expect(recordedArgs).not.toContain(projectPrefix); + expect(stdout).toContain(`Updated pi from ${VERSION} to ${VERSION}`); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); @@ -446,7 +449,8 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); - const fetchMock = vi.fn(async () => Response.json({ version: getNewerPatchVersion() })); + const targetVersion = getNewerPatchVersion(); + const fetchMock = vi.fn(async () => Response.json({ version: targetVersion })); vi.stubGlobal("fetch", fetchMock); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); @@ -458,8 +462,11 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalledOnce(); + const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; - expect(recordedArgs).toContain(PACKAGE_NAME); + expect(recordedArgs).toContain(`${PACKAGE_NAME}@${targetVersion}`); + expect(recordedArgs).not.toContain(PACKAGE_NAME); + expect(stdout).toContain(`Updated pi from ${VERSION} to ${targetVersion}`); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); @@ -509,7 +516,7 @@ else { const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; expect(recordedCalls).toEqual([ expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]), - expect.arrayContaining(["install", "-g", activePackageName]), + expect.arrayContaining(["install", "-g", `${activePackageName}@0.73.0`]), ]); } finally { logSpy.mockRestore(); @@ -565,7 +572,7 @@ if(args.includes("install")) process.exit(23); const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; expect(recordedCalls).toEqual([ expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]), - expect.arrayContaining(["install", "-g", activePackageName]), + expect.arrayContaining(["install", "-g", `${activePackageName}@0.73.0`]), ]); } finally { logSpy.mockRestore(); diff --git a/packages/coding-agent/test/plan-mode-extension.test.ts b/packages/coding-agent/test/plan-mode-extension.test.ts new file mode 100644 index 00000000..419a707e --- /dev/null +++ b/packages/coding-agent/test/plan-mode-extension.test.ts @@ -0,0 +1,167 @@ +import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AssistantMessage } from "@earendil-works/pi-ai"; +import { describe, expect, it, vi } from "vitest"; +import planModeExtension from "../examples/extensions/plan-mode/index.ts"; +import type { ExtensionAPI, ExtensionContext } from "../src/core/extensions/index.ts"; + +type CommandHandler = (args: string, ctx: ExtensionContext) => Promise | void; +type AgentEndHandler = ( + event: { type: "agent_end"; messages: AgentMessage[] }, + ctx: ExtensionContext, +) => Promise | void; + +function createAssistantMessage(text: string): AssistantMessage { + return { + role: "assistant", + content: [{ type: "text", text }], + api: "anthropic-messages", + provider: "anthropic", + model: "mock", + 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 setup(options: { activeTools?: string[]; selectChoice?: string; editorText?: string } = {}) { + let activeTools = options.activeTools ?? ["read", "bash", "edit", "write"]; + const commands = new Map(); + let agentEndHandler: AgentEndHandler | undefined; + + const sendMessage = vi.fn(); + const sendUserMessage = vi.fn(); + const setActiveTools = vi.fn((toolNames) => { + activeTools = [...toolNames]; + }); + const appendEntry = vi.fn(); + + const api = { + registerFlag: vi.fn(), + registerCommand(name: string, command: { handler: CommandHandler }) { + commands.set(name, command.handler); + }, + registerShortcut: vi.fn(), + on(event: string, handler: unknown) { + if (event === "agent_end") agentEndHandler = handler as AgentEndHandler; + }, + getFlag: vi.fn(() => false), + getActiveTools: vi.fn(() => [...activeTools]), + setActiveTools, + sendMessage, + sendUserMessage, + appendEntry, + } as unknown as ExtensionAPI; + + planModeExtension(api); + + const ctx = { + hasUI: true, + ui: { + notify: vi.fn(), + select: vi.fn(async () => options.selectChoice), + editor: vi.fn(async () => options.editorText), + setStatus: vi.fn(), + setWidget: vi.fn(), + theme: { + fg: (_name: string, text: string) => text, + strikethrough: (text: string) => text, + }, + }, + sessionManager: { getEntries: () => [] }, + isIdle: () => false, + hasPendingMessages: () => false, + } as unknown as ExtensionContext; + + async function runCommand(name: string): Promise { + const command = commands.get(name); + if (!command) throw new Error(`Missing command: ${name}`); + await command("", ctx); + } + + async function triggerAgentEnd(text: string): Promise { + if (!agentEndHandler) throw new Error("Missing agent_end handler"); + await agentEndHandler({ type: "agent_end", messages: [createAssistantMessage(text)] }, ctx); + } + + return { + activeTools: () => activeTools, + appendEntry, + ctx, + runCommand, + sendMessage, + sendUserMessage, + setActiveTools, + triggerAgentEnd, + }; +} + +describe("plan-mode example extension", () => { + it("preserves custom active tools while toggling plan mode", async () => { + const { activeTools, runCommand, setActiveTools } = setup({ + activeTools: ["read", "bash", "edit", "write", "echo_tool"], + }); + + await runCommand("plan"); + + expect(activeTools()).toEqual(["read", "bash", "echo_tool", "grep", "find", "ls", "questionnaire"]); + expect(setActiveTools).toHaveBeenLastCalledWith([ + "read", + "bash", + "echo_tool", + "grep", + "find", + "ls", + "questionnaire", + ]); + + await runCommand("plan"); + + expect(activeTools()).toEqual(["read", "bash", "edit", "write", "echo_tool"]); + expect(setActiveTools).toHaveBeenLastCalledWith(["read", "bash", "edit", "write", "echo_tool"]); + }); + + it("does not prompt when the assistant response contains no plan", async () => { + const { ctx, runCommand, sendMessage, triggerAgentEnd } = setup(); + + await runCommand("plan"); + await triggerAgentEnd("This file defines the command-line argument parser."); + + expect(ctx.ui.select).not.toHaveBeenCalled(); + expect(sendMessage).not.toHaveBeenCalled(); + }); + + it("queues plan refinement as a follow-up user message", async () => { + const { runCommand, sendUserMessage, triggerAgentEnd } = setup({ + selectChoice: "Refine the plan", + editorText: "Add a regression test.", + }); + + await runCommand("plan"); + await triggerAgentEnd("Plan:\n1. Inspect the current implementation\n2. Add a regression test"); + + expect(sendUserMessage).toHaveBeenCalledWith("Add a regression test.", { deliverAs: "followUp" }); + }); + + it("queues plan execution as a follow-up custom message", async () => { + const { activeTools, runCommand, sendMessage, triggerAgentEnd } = setup({ + activeTools: ["read", "bash", "edit", "write", "echo_tool"], + selectChoice: "Execute the plan (track progress)", + }); + + await runCommand("plan"); + await triggerAgentEnd("Plan:\n1. Inspect the current implementation\n2. Add a regression test"); + + expect(activeTools()).toEqual(["read", "bash", "edit", "write", "echo_tool"]); + expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ customType: "plan-mode-execute" }), { + triggerTurn: true, + deliverAs: "followUp", + }); + }); +}); diff --git a/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts b/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts index 08b3a56f..5e4d5b08 100644 --- a/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts +++ b/packages/coding-agent/test/rpc-prompt-response-semantics.test.ts @@ -8,7 +8,7 @@ import { EventStream, getModel, type Model, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it, vi } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; import type { AgentSessionRuntime } from "../src/core/agent-session-runtime.ts"; diff --git a/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts b/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts index a6338d14..ff6a9798 100644 --- a/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts +++ b/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts @@ -20,11 +20,11 @@ import { type Model, type SimpleStreamOptions, Type, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/compat"; import { getOpenAICodexWebSocketDebugStats, - streamSimpleOpenAICodexResponses, -} from "../../ai/src/providers/openai-codex-responses.ts"; + streamSimple as streamSimpleOpenAICodexResponses, +} from "../../ai/src/api/openai-codex-responses.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; import { createExtensionRuntime } from "../src/core/extensions/loader.ts"; import type { ToolDefinition } from "../src/core/extensions/types.ts"; diff --git a/packages/coding-agent/test/sdk-openrouter-attribution.test.ts b/packages/coding-agent/test/sdk-openrouter-attribution.test.ts index b03e5bb6..bae02aa4 100644 --- a/packages/coding-agent/test/sdk-openrouter-attribution.test.ts +++ b/packages/coding-agent/test/sdk-openrouter-attribution.test.ts @@ -6,6 +6,7 @@ import { type AssistantMessage, createAssistantMessageEventStream, type Model, + type ProviderHeaders, type SimpleStreamOptions, } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -88,7 +89,7 @@ describe("createAgentSession provider attribution headers", () => { requestHeaders?: Record; sessionId?: string; } = {}, - ): Promise | undefined> { + ): Promise { const settingsManager = SettingsManager.create(cwd, agentDir); if (options.telemetryEnabled === false) { settingsManager.setEnableInstallTelemetry(false); diff --git a/packages/coding-agent/test/sdk-session-manager.test.ts b/packages/coding-agent/test/sdk-session-manager.test.ts index cb7e505c..9cdf7774 100644 --- a/packages/coding-agent/test/sdk-session-manager.test.ts +++ b/packages/coding-agent/test/sdk-session-manager.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createAgentSession } from "../src/core/sdk.ts"; import { SessionManager } from "../src/core/session-manager.ts"; diff --git a/packages/coding-agent/test/session-id-readonly.test.ts b/packages/coding-agent/test/session-id-readonly.test.ts index b6e97ce8..47537b60 100644 --- a/packages/coding-agent/test/session-id-readonly.test.ts +++ b/packages/coding-agent/test/session-id-readonly.test.ts @@ -1,5 +1,14 @@ import { spawn } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; @@ -15,7 +24,10 @@ afterEach(() => { }); function createTempDir(): string { - const dir = mkdtempSync(join(tmpdir(), "pi-session-id-readonly-")); + // realpath: on macOS tmpdir() is a symlink (/var -> /private/var), but the + // spawned CLI sees the physical path via process.cwd(). Session cwd + // filtering compares paths textually, so the fixture must use physical paths. + const dir = realpathSync(mkdtempSync(join(tmpdir(), "pi-session-id-readonly-"))); tempDirs.push(dir); return dir; } diff --git a/packages/coding-agent/test/session-selector-path-delete.test.ts b/packages/coding-agent/test/session-selector-path-delete.test.ts index 52cae486..cb702d5e 100644 --- a/packages/coding-agent/test/session-selector-path-delete.test.ts +++ b/packages/coding-agent/test/session-selector-path-delete.test.ts @@ -282,6 +282,45 @@ describe("session selector path/delete interactions", () => { expect(output).toContain("└─ Child"); }); + it("sorts threaded sessions by latest activity in their subtree", async () => { + const parentOne = makeSession({ + id: "parent-one", + name: "Parent one", + modified: new Date("2026-01-02T00:00:00.000Z"), + }); + const parentTwo = makeSession({ + id: "parent-two", + name: "Parent two", + modified: new Date("2026-01-01T00:00:00.000Z"), + }); + const childTwo = makeSession({ + id: "child-two", + name: "Child two", + parentSessionPath: parentTwo.path, + modified: new Date("2026-01-03T00:00:00.000Z"), + }); + + const selector = new SessionSelectorComponent( + async () => [parentOne, parentTwo, childTwo], + async () => [], + () => {}, + () => {}, + () => {}, + () => {}, + { keybindings }, + ); + await flushPromises(); + + const output = stripAnsi(selector.render(120).join("\n")); + const parentTwoIndex = output.indexOf("Parent two"); + const childTwoIndex = output.indexOf("└─ Child two"); + const parentOneIndex = output.indexOf("Parent one"); + + expect(parentTwoIndex).toBeGreaterThanOrEqual(0); + expect(childTwoIndex).toBeGreaterThan(parentTwoIndex); + expect(parentOneIndex).toBeGreaterThan(childTwoIndex); + }); + it("treats the current session as active across symlink aliases", async () => { const paths = createSymlinkedSessionPaths(); tempDirs.push(paths.baseDir); diff --git a/packages/coding-agent/test/settings-manager.test.ts b/packages/coding-agent/test/settings-manager.test.ts index 279bece1..d86c3f91 100644 --- a/packages/coding-agent/test/settings-manager.test.ts +++ b/packages/coding-agent/test/settings-manager.test.ts @@ -198,6 +198,24 @@ describe("SettingsManager", () => { }); }); + describe("theme setting", () => { + it("stores slash-separated automatic theme settings separately from fixed theme names", async () => { + const settingsPath = join(agentDir, "settings.json"); + writeFileSync(settingsPath, JSON.stringify({ theme: "light/dark" })); + + const manager = SettingsManager.create(projectDir, agentDir); + + expect(manager.getTheme()).toBeUndefined(); + expect(manager.getThemeSetting()).toBe("light/dark"); + + manager.setTheme("solarized-light/tokyo-night"); + await manager.flush(); + + const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")); + expect(savedSettings.theme).toBe("solarized-light/tokyo-night"); + }); + }); + describe("error tracking", () => { it("should collect and clear load errors via drainErrors", () => { const globalSettingsPath = join(agentDir, "settings.json"); diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index e04732d0..ba5ae5c7 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -5,6 +5,7 @@ import { type Model, } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { estimateTokens } from "../../src/core/compaction/index.ts"; import { createHarness, type Harness } from "./harness.ts"; type SessionWithCompactionInternals = { @@ -67,19 +68,20 @@ function useSummaryStreamFn(harness: Harness, summary: string): () => number { } function seedCompactableSession(harness: Harness): void { + harness.settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); const now = Date.now(); harness.sessionManager.appendMessage({ role: "user", content: [{ type: "text", text: "message to compact" }], timestamp: now - 1000, }); - harness.sessionManager.appendMessage( - createAssistant(harness, { - stopReason: "stop", - totalTokens: 100, - timestamp: now - 500, - }), - ); + const assistant = createAssistant(harness, { + stopReason: "stop", + totalTokens: 100, + timestamp: now - 500, + }); + assistant.content = [{ type: "text", text: "assistant response to compact" }]; + harness.sessionManager.appendMessage(assistant); harness.session.agent.state.messages = harness.sessionManager.buildSessionContext().messages; } @@ -96,6 +98,7 @@ describe("AgentSession compaction characterization", () => { it("manually compacts using an extension-provided summary", async () => { const harness = await createHarness({ + settings: { compaction: { keepRecentTokens: 1 } }, extensionFactories: [ (pi) => { pi.on("session_before_compact", async (event) => ({ @@ -116,8 +119,10 @@ describe("AgentSession compaction characterization", () => { const result = await harness.session.compact(); const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction"); + const estimatedTokensAfter = harness.session.messages.reduce((sum, message) => sum + estimateTokens(message), 0); expect(result.summary).toBe("summary from extension"); + expect(result.estimatedTokensAfter).toBe(estimatedTokensAfter); expect(compactionEntries).toHaveLength(1); expect(harness.session.messages[0]?.role).toBe("compactionSummary"); }); @@ -145,7 +150,7 @@ describe("AgentSession compaction characterization", () => { const result = await harness.session.compact(); - expect(result.summary).toBe("summary from custom stream"); + expect(result.summary).toContain("summary from custom stream"); expect(getStreamCallCount()).toBe(1); }); @@ -159,12 +164,15 @@ describe("AgentSession compaction characterization", () => { await sessionInternals._runAutoCompaction("threshold", false); const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction"); + const compactionEnd = harness.eventsOfType("compaction_end").at(-1); expect(compactionEntries).toHaveLength(1); + expect(compactionEnd?.result?.estimatedTokensAfter).toBeGreaterThan(0); expect(getStreamCallCount()).toBe(1); }); it("cancels in-progress manual compaction when abortCompaction is called", async () => { const harness = await createHarness({ + settings: { compaction: { keepRecentTokens: 1 } }, extensionFactories: [ (pi) => { pi.on("session_before_compact", async (event) => { @@ -248,6 +256,37 @@ describe("AgentSession compaction characterization", () => { ); }); + it("compacts successful overflow responses without retrying", async () => { + const harness = await createHarness({ + settings: { compaction: { enabled: true, keepRecentTokens: 1, reserveTokens: 0 } }, + models: [{ id: "faux-1", contextWindow: 1, maxTokens: 100 }], + extensionFactories: [ + (pi) => { + pi.on("session_before_compact", async (event) => ({ + compaction: { + summary: "successful overflow compacted", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: {}, + }, + })); + }, + ], + }); + harnesses.push(harness); + harness.setResponses([fauxAssistantMessage("completed answer")]); + + await expect(harness.session.prompt("hello")).resolves.toBeUndefined(); + + const compactionEnd = harness.eventsOfType("compaction_end").at(-1); + expect(compactionEnd).toMatchObject({ + reason: "overflow", + aborted: false, + willRetry: false, + }); + expect(harness.faux.state.callCount).toBe(1); + }); + it("ignores stale pre-compaction assistant usage on pre-prompt checks", async () => { const harness = await createHarness(); harnesses.push(harness); diff --git a/packages/coding-agent/test/suite/agent-session-runtime.test.ts b/packages/coding-agent/test/suite/agent-session-runtime.test.ts index 69c817ac..cf1a81c2 100644 --- a/packages/coding-agent/test/suite/agent-session-runtime.test.ts +++ b/packages/coding-agent/test/suite/agent-session-runtime.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, parse } from "node:path"; -import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; +import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import { type CreateAgentSessionRuntimeFactory, diff --git a/packages/coding-agent/test/suite/harness.ts b/packages/coding-agent/test/suite/harness.ts index cec82013..0fb95825 100644 --- a/packages/coding-agent/test/suite/harness.ts +++ b/packages/coding-agent/test/suite/harness.ts @@ -7,8 +7,13 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentMessage, AgentTool } from "@earendil-works/pi-agent-core"; import { Agent } from "@earendil-works/pi-agent-core"; -import type { FauxModelDefinition, FauxProviderRegistration, FauxResponseStep, Model } from "@earendil-works/pi-ai"; -import { registerFauxProvider } from "@earendil-works/pi-ai"; +import type { + FauxModelDefinition, + FauxProviderRegistration, + FauxResponseStep, + Model, +} from "@earendil-works/pi-ai/compat"; +import { registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { AgentSession, type AgentSessionEvent } from "../../src/core/agent-session.ts"; import { AuthStorage } from "../../src/core/auth-storage.ts"; import type { ExtensionRunner } from "../../src/core/extensions/index.ts"; diff --git a/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts b/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts index 17d9b302..81d3f275 100644 --- a/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts +++ b/packages/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { registerFauxProvider } from "@earendil-works/pi-ai"; +import { registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import { type CreateAgentSessionRuntimeFactory, diff --git a/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts b/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts index e0003aa6..198b45a8 100644 --- a/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts +++ b/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { DefaultResourceLoader } from "../../../src/core/resource-loader.ts"; diff --git a/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts b/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts index 0e4c061e..b6f7002e 100644 --- a/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts +++ b/packages/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; +import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import type { AgentSession } from "../../../src/core/agent-session.ts"; import { diff --git a/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts b/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts index d7d1376c..3d900ee3 100644 --- a/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts +++ b/packages/coding-agent/test/suite/regressions/3592-no-builtin-tools-keeps-extension-tools.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getModel } from "@earendil-works/pi-ai"; +import { getModel } from "@earendil-works/pi-ai/compat"; import { Type } from "typebox"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { diff --git a/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts b/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts index 33a960e2..9ccf77cd 100644 --- a/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts +++ b/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts @@ -21,6 +21,7 @@ type ShutdownThis = { unregisterSignalHandlers: () => void; runtimeHost: { dispose: () => Promise }; ui: { terminal: { drainInput: (ms: number) => Promise } }; + themeController: { disableAutoSync: () => void }; stop: () => void; sessionManager: SessionManager; }; @@ -81,6 +82,7 @@ function createContext(order: string[], sessionManager = createSessionManager()) }), }, }, + themeController: { disableAutoSync: vi.fn() }, stop: vi.fn(() => { order.push("stop"); }), diff --git a/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts b/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts new file mode 100644 index 00000000..9af206fe --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts @@ -0,0 +1,95 @@ +import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { afterEach, describe, expect, it } from "vitest"; +import type { ExtensionFactory } from "../../../src/index.ts"; +import { createHarness, type Harness } from "../harness.ts"; + +type SessionWithCompactionInternals = { + _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; +}; + +interface RecordedCompactionEvent { + type: "session_before_compact" | "session_compact"; + reason: "manual" | "threshold" | "overflow"; + willRetry: boolean; +} + +function recordingExtension(recorded: RecordedCompactionEvent[]): ExtensionFactory { + return (pi) => { + pi.on("session_before_compact", async (event) => { + recorded.push({ type: event.type, reason: event.reason, willRetry: event.willRetry }); + return { + compaction: { + summary: "summary from extension", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: {}, + }, + }; + }); + pi.on("session_compact", async (event) => { + recorded.push({ type: event.type, reason: event.reason, willRetry: event.willRetry }); + }); + }; +} + +async function createCompactionHarness(recorded: RecordedCompactionEvent[]): Promise { + const harness = await createHarness({ + settings: { compaction: { keepRecentTokens: 1 } }, + extensionFactories: [recordingExtension(recorded)], + }); + harness.setResponses([fauxAssistantMessage("one"), fauxAssistantMessage("two")]); + await harness.session.prompt("first"); + await harness.session.prompt("second"); + return harness; +} + +describe("issue #5217 compaction reason on extension events", () => { + const harnesses: Harness[] = []; + + afterEach(() => { + while (harnesses.length > 0) { + harnesses.pop()?.cleanup(); + } + }); + + it("reports manual reason for compact()", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + + await harness.session.compact(); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "manual", willRetry: false }, + { type: "session_compact", reason: "manual", willRetry: false }, + ]); + }); + + it("reports threshold reason for auto-compaction", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + const sessionInternals = harness.session as unknown as SessionWithCompactionInternals; + + await sessionInternals._runAutoCompaction("threshold", false); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "threshold", willRetry: false }, + { type: "session_compact", reason: "threshold", willRetry: false }, + ]); + }); + + it("reports overflow reason and willRetry for overflow recovery", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + const sessionInternals = harness.session as unknown as SessionWithCompactionInternals; + + await sessionInternals._runAutoCompaction("overflow", true); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "overflow", willRetry: true }, + { type: "session_compact", reason: "overflow", willRetry: true }, + ]); + }); +}); diff --git a/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts b/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts index 58bc17f8..296993e0 100644 --- a/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts +++ b/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts @@ -2,7 +2,7 @@ import { existsSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; +import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai/compat"; import { afterEach, describe, expect, it } from "vitest"; import { AgentSession } from "../../../src/core/agent-session.ts"; import { AuthStorage } from "../../../src/core/auth-storage.ts"; diff --git a/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts b/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts index d170f1cf..75e82fb7 100644 --- a/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts +++ b/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts @@ -13,6 +13,7 @@ type ShutdownThis = { unregisterSignalHandlers: () => void; runtimeHost: { dispose: () => Promise }; ui: { terminal: { drainInput: (ms: number) => Promise } }; + themeController: { disableAutoSync: () => void }; stop: () => void; }; @@ -73,6 +74,7 @@ describe("InteractiveMode SIGTERM shutdown with signal-exit (#5724)", () => { }), }, }, + themeController: { disableAutoSync: vi.fn() }, stop: vi.fn(() => { order.push("stop"); }), diff --git a/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts b/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts new file mode 100644 index 00000000..2c8d88e0 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts @@ -0,0 +1,513 @@ +import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { Container, Text } from "@earendil-works/pi-tui"; +import { describe, expect, it, vi } from "vitest"; +import type { AgentSessionEvent } from "../../../src/core/agent-session.ts"; +import type { ExtensionUIContext } from "../../../src/core/extensions/index.ts"; +import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.ts"; +import { initTheme, type Theme, theme } from "../../../src/modes/interactive/theme/theme.ts"; +import { createHarness } from "../harness.ts"; + +function createUiContext( + onNotify: (message: string, type: "info" | "warning" | "error" | undefined) => void, +): ExtensionUIContext { + return { + select: async () => undefined, + confirm: async () => false, + input: async () => undefined, + notify: onNotify, + onTerminalInput: () => () => {}, + setStatus: () => {}, + setWorkingMessage: () => {}, + setWorkingVisible: () => {}, + setWorkingIndicator: () => {}, + setHiddenThinkingLabel: () => {}, + setWidget: () => {}, + setFooter: () => {}, + setHeader: () => {}, + setTitle: () => {}, + custom: async () => undefined as T, + pasteToEditor: () => {}, + setEditorText: () => {}, + getEditorText: () => "", + editor: async () => undefined, + addAutocompleteProvider: () => {}, + setEditorComponent: () => {}, + getEditorComponent: () => undefined, + get theme() { + return theme; + }, + getAllThemes: () => [], + getTheme: () => undefined, + setTheme: (_theme: string | Theme) => ({ success: false, error: "Theme switching not available in tests" }), + getToolsExpanded: () => false, + setToolsExpanded: () => {}, + }; +} + +type LoadedResourcesResult = { [K in keyof T]: T[K] } & { diagnostics: [] }; + +type LoadedResourcesContext = { + loadedResourcesContainer: Container; + chatContainer: Container; + options: { verbose?: boolean }; + settingsManager: { getQuietStartup: () => boolean }; + sessionManager: { getCwd: () => string }; + session: { + promptTemplates: []; + resourceLoader: { + getAgentsFiles: () => LoadedResourcesResult<{ agentsFiles: Array<{ path: string }> }>; + getSkills: () => LoadedResourcesResult<{ skills: [] }>; + getPrompts: () => LoadedResourcesResult<{ prompts: [] }>; + getThemes: () => LoadedResourcesResult<{ themes: [] }>; + getExtensions: () => { extensions: []; errors: [] }; + }; + extensionRunner: { + getCommandDiagnostics: () => []; + getShortcutDiagnostics: () => []; + getRegisteredCommands: () => []; + }; + }; + getStartupExpansionState: () => boolean; + formatDisplayPath: (resourcePath: string) => string; + formatContextPath: (resourcePath: string) => string; + getBuiltInCommandConflictDiagnostics: (extensionRunner: LoadedResourcesContext["session"]["extensionRunner"]) => []; +}; + +type RebindContext = { + unsubscribe?: () => void; + applyRuntimeSettings: () => void; + renderCurrentSessionState: () => void; + bindCurrentSessionExtensions: () => Promise; + subscribeToAgent: () => void; + updateAvailableProviderCount: () => Promise; + updateEditorBorderColor: () => void; + updateTerminalTitle: () => void; +}; + +type ReloadCommandContext = { + hideThinkingBlock: boolean; + session: { + isStreaming: boolean; + isCompacting: boolean; + reload: (options?: { beforeSessionStart?: () => void | Promise }) => Promise; + resourceLoader: { getThemes: () => { themes: [] } }; + extensionRunner: unknown; + modelRegistry: { getError: () => string | undefined }; + }; + settingsManager: { + getHttpIdleTimeoutMs: () => number; + getHideThinkingBlock: () => boolean; + getEditorPaddingX: () => number; + getAutocompleteMaxVisible: () => number; + getShowHardwareCursor: () => boolean; + getClearOnShrink: () => boolean; + }; + keybindings: { reload: () => void }; + customHeader?: unknown; + builtInHeader?: unknown; + editorContainer: { clear: () => void; addChild: (component: unknown) => void }; + ui: { + setFocus: (component: unknown) => void; + requestRender: (force?: boolean) => void; + setShowHardwareCursor: (enabled: boolean) => void; + setClearOnShrink: (enabled: boolean) => void; + }; + editor: unknown; + defaultEditor: { setPaddingX: (padding: number) => void; setAutocompleteMaxVisible: (maxVisible: number) => void }; + themeController: { applyFromSettings: () => Promise }; + resetExtensionUI: () => void; + rebuildChatFromMessages: () => void; + setupAutocompleteProvider: () => void; + setupExtensionShortcuts: (runner: unknown) => void; + showLoadedResources: (options: unknown) => void; + maybeSaveImplicitProjectTrustAfterReload: () => boolean; + showStatus: (message: string) => void; + showWarning: (message: string) => void; + showError: (message: string) => void; +}; + +type InteractiveModePrototype = { + showLoadedResources( + this: LoadedResourcesContext, + options?: { extensions?: Array<{ path: string }>; force?: boolean; showDiagnosticsWhenQuiet?: boolean }, + ): void; + rebindCurrentSession(this: RebindContext, options?: { renderBeforeBind?: boolean }): Promise; + handleReloadCommand(this: ReloadCommandContext): Promise; +}; + +const interactiveModePrototype = InteractiveMode.prototype as unknown as InteractiveModePrototype; + +type ReloadCommandContextOverrides = Omit< + Partial, + "session" | "settingsManager" | "keybindings" | "editorContainer" | "ui" | "defaultEditor" | "themeController" +> & { + session?: Partial; + settingsManager?: Partial; + keybindings?: Partial; + editorContainer?: Partial; + ui?: Partial; + defaultEditor?: Partial; + themeController?: Partial; +}; + +function createReloadCommandContext(overrides: ReloadCommandContextOverrides = {}): ReloadCommandContext { + const editor = overrides.editor ?? {}; + return { + hideThinkingBlock: overrides.hideThinkingBlock ?? false, + session: { + isStreaming: false, + isCompacting: false, + reload: async (options) => { + await options?.beforeSessionStart?.(); + }, + resourceLoader: { getThemes: () => ({ themes: [] }) }, + extensionRunner: {}, + modelRegistry: { getError: () => undefined }, + ...overrides.session, + }, + settingsManager: { + getHttpIdleTimeoutMs: () => 0, + getHideThinkingBlock: () => false, + getEditorPaddingX: () => 1, + getAutocompleteMaxVisible: () => 10, + getShowHardwareCursor: () => false, + getClearOnShrink: () => false, + ...overrides.settingsManager, + }, + keybindings: { reload: () => {}, ...overrides.keybindings }, + editorContainer: { clear: () => {}, addChild: () => {}, ...overrides.editorContainer }, + ui: { + setFocus: () => {}, + requestRender: () => {}, + setShowHardwareCursor: () => {}, + setClearOnShrink: () => {}, + ...overrides.ui, + }, + editor, + defaultEditor: { setPaddingX: () => {}, setAutocompleteMaxVisible: () => {}, ...overrides.defaultEditor }, + themeController: { applyFromSettings: async () => {}, ...overrides.themeController }, + customHeader: overrides.customHeader, + builtInHeader: overrides.builtInHeader, + resetExtensionUI: overrides.resetExtensionUI ?? (() => {}), + rebuildChatFromMessages: overrides.rebuildChatFromMessages ?? (() => {}), + setupAutocompleteProvider: overrides.setupAutocompleteProvider ?? (() => {}), + setupExtensionShortcuts: overrides.setupExtensionShortcuts ?? (() => {}), + showLoadedResources: overrides.showLoadedResources ?? (() => {}), + maybeSaveImplicitProjectTrustAfterReload: overrides.maybeSaveImplicitProjectTrustAfterReload ?? (() => false), + showStatus: overrides.showStatus ?? (() => {}), + showWarning: overrides.showWarning ?? (() => {}), + showError: overrides.showError ?? (() => {}), + }; +} + +type MessageEvent = Extract; + +function getMessageText(event: MessageEvent): string { + const message = event.message; + if (!("content" in message)) { + return ""; + } + const content = message.content; + if (typeof content === "string") { + return content; + } + return content + .filter((part): part is { type: "text"; text: string } => part.type === "text") + .map((part) => part.text) + .join(""); +} + +function createLoadedResourcesContext(): LoadedResourcesContext { + return { + loadedResourcesContainer: new Container(), + chatContainer: new Container(), + options: { verbose: true }, + settingsManager: { getQuietStartup: () => false }, + sessionManager: { getCwd: () => "/repo" }, + session: { + promptTemplates: [], + resourceLoader: { + getAgentsFiles: () => ({ agentsFiles: [{ path: "/repo/AGENTS.md" }], diagnostics: [] }), + getSkills: () => ({ skills: [], diagnostics: [] }), + getPrompts: () => ({ prompts: [], diagnostics: [] }), + getThemes: () => ({ themes: [], diagnostics: [] }), + getExtensions: () => ({ extensions: [], errors: [] }), + }, + extensionRunner: { + getCommandDiagnostics: () => [], + getShortcutDiagnostics: () => [], + getRegisteredCommands: () => [], + }, + }, + getStartupExpansionState: () => false, + formatDisplayPath: (resourcePath) => resourcePath, + formatContextPath: (resourcePath) => resourcePath.replace("/repo/", ""), + getBuiltInCommandConflictDiagnostics: () => [], + }; +} + +describe("regression #5943: session_start transient UI", () => { + it("renders loaded resources before restored messages without stale entries", () => { + initTheme("dark", false); + const context = createLoadedResourcesContext(); + const root = new Container(); + root.addChild(context.loadedResourcesContainer); + root.addChild(context.chatContainer); + context.loadedResourcesContainer.addChild(new Text("stale resources", 0, 0)); + context.chatContainer.addChild(new Text("restored message", 0, 0)); + + interactiveModePrototype.showLoadedResources.call(context); + + const chatRendered = context.chatContainer.render(80).join("\n"); + expect(chatRendered).toContain("restored message"); + expect(chatRendered).not.toContain("[Context]"); + + const rendered = root.render(80).join("\n"); + expect(rendered).not.toContain("stale resources"); + expect(rendered.indexOf("[Context]")).toBeLessThan(rendered.indexOf("restored message")); + }); + + it("renders replacement session state before session_start handlers can notify", async () => { + const events: string[] = []; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", (_event, ctx) => { + ctx.ui.notify("Hello Error", "error"); + }); + }, + ], + }); + + try { + const context: RebindContext = { + applyRuntimeSettings: () => events.push("apply"), + renderCurrentSessionState: () => events.push("render"), + bindCurrentSessionExtensions: async () => { + events.push("bind"); + await harness.session.bindExtensions({ + uiContext: createUiContext((message) => events.push(`notify:${message}`)), + mode: "tui", + }); + }, + subscribeToAgent: () => events.push("subscribe"), + updateAvailableProviderCount: async () => {}, + updateEditorBorderColor: () => {}, + updateTerminalTitle: () => {}, + }; + + await interactiveModePrototype.rebindCurrentSession.call(context, { renderBeforeBind: true }); + + expect(events).toEqual(["apply", "render", "subscribe", "bind", "notify:Hello Error"]); + } finally { + harness.cleanup(); + } + }); + + it("subscribes before replacement session_start handlers send messages", async () => { + const events: string[] = []; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", () => { + pi.sendMessage({ + customType: "session-start", + content: "custom from start", + display: true, + }); + }); + }, + ], + }); + + try { + const context: RebindContext = { + applyRuntimeSettings: () => {}, + renderCurrentSessionState: () => events.push("render"), + bindCurrentSessionExtensions: async () => { + events.push("bind"); + await harness.session.bindExtensions({ + uiContext: createUiContext(() => {}), + mode: "tui", + }); + }, + subscribeToAgent: () => { + events.push("subscribe"); + harness.session.subscribe((event) => { + if (event.type !== "message_start" && event.type !== "message_end") { + return; + } + events.push(`${event.type}:${event.message.role}:${getMessageText(event)}`); + }); + }, + updateAvailableProviderCount: async () => {}, + updateEditorBorderColor: () => {}, + updateTerminalTitle: () => {}, + }; + + await interactiveModePrototype.rebindCurrentSession.call(context, { renderBeforeBind: true }); + + expect(events).toEqual([ + "render", + "subscribe", + "bind", + "message_start:custom:custom from start", + "message_end:custom:custom from start", + ]); + } finally { + harness.cleanup(); + } + }); + + it("subscribes before replacement session_start handlers send user messages", async () => { + const events: string[] = []; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", () => { + pi.sendUserMessage("user from start"); + }); + }, + ], + }); + harness.setResponses([fauxAssistantMessage("assistant from start")]); + + try { + const context: RebindContext = { + applyRuntimeSettings: () => {}, + renderCurrentSessionState: () => events.push("render"), + bindCurrentSessionExtensions: async () => { + events.push("bind"); + await harness.session.bindExtensions({ + uiContext: createUiContext(() => {}), + mode: "tui", + }); + }, + subscribeToAgent: () => { + events.push("subscribe"); + harness.session.subscribe((event) => { + if (event.type !== "message_start" && event.type !== "message_end") { + return; + } + events.push(`${event.type}:${event.message.role}:${getMessageText(event)}`); + }); + }, + updateAvailableProviderCount: async () => {}, + updateEditorBorderColor: () => {}, + updateTerminalTitle: () => {}, + }; + + await interactiveModePrototype.rebindCurrentSession.call(context, { renderBeforeBind: true }); + await harness.session.agent.waitForIdle(); + + expect(events.slice(0, 3)).toEqual(["render", "subscribe", "bind"]); + expect(events).toContain("message_start:user:user from start"); + expect(events).toContain("message_end:user:user from start"); + expect(events).toContain("message_end:assistant:assistant from start"); + } finally { + harness.cleanup(); + } + }); + + it("runs the reload render hook before reload session_start handlers can notify", async () => { + const events: string[] = []; + const beforeSessionStart = vi.fn(() => { + events.push("render"); + }); + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", (event, ctx) => { + events.push(`start:${event.reason}`); + ctx.ui.notify(`notify:${event.reason}`, "error"); + }); + }, + ], + }); + + try { + await harness.session.bindExtensions({ + uiContext: createUiContext((message) => events.push(message)), + mode: "tui", + }); + expect(events).toEqual(["start:startup", "notify:startup"]); + + events.length = 0; + await harness.session.reload({ beforeSessionStart }); + + expect(beforeSessionStart).toHaveBeenCalledTimes(1); + expect(events).toEqual(["render", "start:reload", "notify:reload"]); + } finally { + harness.cleanup(); + } + }); + + it("refreshes hideThinkingBlock before rebuilding chat during reload", async () => { + initTheme("dark", false); + const events: string[] = []; + let context: ReloadCommandContext; + context = createReloadCommandContext({ + settingsManager: { getHideThinkingBlock: () => true }, + session: { + reload: async (options) => { + events.push("reload"); + await options?.beforeSessionStart?.(); + events.push(`start:${context.hideThinkingBlock}`); + }, + }, + rebuildChatFromMessages: () => { + events.push(`rebuild:${context.hideThinkingBlock}`); + }, + }); + + await interactiveModePrototype.handleReloadCommand.call(context); + + expect(context.hideThinkingBlock).toBe(true); + expect(events).toEqual(["reload", "rebuild:true", "start:true"]); + }); + + it("keeps the reload blocker focused until async reload completes", async () => { + initTheme("dark", false); + const editor = {}; + let focused: unknown; + let chatRestored = false; + let markReloadWaiting!: () => void; + let finishReload!: () => void; + const reloadWaiting = new Promise((resolve) => { + markReloadWaiting = resolve; + }); + const reloadFinished = new Promise((resolve) => { + finishReload = resolve; + }); + + const context = createReloadCommandContext({ + editor, + session: { + reload: async (options) => { + await options?.beforeSessionStart?.(); + markReloadWaiting(); + await reloadFinished; + }, + }, + ui: { + setFocus: (component) => { + focused = component; + }, + }, + rebuildChatFromMessages: () => { + chatRestored = true; + }, + }); + + const reloadPromise = interactiveModePrototype.handleReloadCommand.call(context); + await reloadWaiting; + + expect(chatRestored).toBe(true); + expect(focused).not.toBe(editor); + + finishReload(); + await reloadPromise; + + expect(focused).toBe(editor); + }); +}); diff --git a/packages/coding-agent/test/suite/regressions/5996-session-name-newlines.test.ts b/packages/coding-agent/test/suite/regressions/5996-session-name-newlines.test.ts new file mode 100644 index 00000000..c33f7760 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5996-session-name-newlines.test.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it } from "vitest"; +import type { ExtensionAPI } from "../../../src/index.ts"; +import { createHarness, type Harness } from "../harness.ts"; + +describe("regression #5996: session names do not contain newlines", () => { + const harnesses: Harness[] = []; + + afterEach(() => { + while (harnesses.length > 0) { + harnesses.pop()?.cleanup(); + } + }); + + it("filters newlines when AgentSession.setSessionName is called", async () => { + const harness = await createHarness(); + harnesses.push(harness); + + harness.session.setSessionName("hello\nworld\r\nagain"); + + expect(harness.sessionManager.getSessionName()).toBe("hello world again"); + expect(harness.eventsOfType("session_info_changed").map((event) => event.name)).toEqual(["hello world again"]); + }); + + it("filters newlines when an extension calls pi.setSessionName", async () => { + let api: ExtensionAPI | undefined; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + api = pi; + }, + ], + }); + harnesses.push(harness); + + api?.setSessionName("from\nextension"); + + expect(harness.sessionManager.getSessionName()).toBe("from extension"); + expect(harness.eventsOfType("session_info_changed").map((event) => event.name)).toEqual(["from extension"]); + }); +}); diff --git a/packages/coding-agent/test/suite/regressions/6019-explicit-provider-retry-message.test.ts b/packages/coding-agent/test/suite/regressions/6019-explicit-provider-retry-message.test.ts new file mode 100644 index 00000000..aa4889c3 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/6019-explicit-provider-retry-message.test.ts @@ -0,0 +1,31 @@ +import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { describe, expect, it } from "vitest"; +import { createHarness } from "../harness.ts"; + +const openAIExplicitRetryMessage = + "An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID req_******** in your message."; +const bedrockExplicitRetryMessage = + '{"message":"The system encountered an unexpected error during processing. Try your request again."}'; + +describe("regression: issue 6019 explicit provider retry messages", () => { + it.each([ + ["openai", openAIExplicitRetryMessage], + ["bedrock", bedrockExplicitRetryMessage], + ])("retries %s explicit retry guidance", async (_provider, errorMessage) => { + const harness = await createHarness({ settings: { retry: { enabled: true, maxRetries: 3, baseDelayMs: 1 } } }); + try { + harness.setResponses([ + fauxAssistantMessage("", { stopReason: "error", errorMessage }), + fauxAssistantMessage("recovered"), + ]); + + await harness.session.prompt("test"); + + expect(harness.faux.state.callCount).toBe(2); + expect(harness.eventsOfType("auto_retry_start").map((event) => event.errorMessage)).toEqual([errorMessage]); + expect(harness.eventsOfType("auto_retry_end").map((event) => event.success)).toEqual([true]); + } finally { + harness.cleanup(); + } + }); +}); diff --git a/packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts b/packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts new file mode 100644 index 00000000..ced01224 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts @@ -0,0 +1,131 @@ +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { clearExtensionCache, loadExtensions, loadExtensionsCached } from "../../../src/core/extensions/loader.ts"; +import { DefaultResourceLoader } from "../../../src/core/resource-loader.ts"; + +interface TestState { + moduleLoads?: number; + factoryRuns?: number; +} + +function state(): TestState { + const global = globalThis as typeof globalThis & { __extensionFactoryCacheTest?: TestState }; + if (!global.__extensionFactoryCacheTest) { + global.__extensionFactoryCacheTest = {}; + } + return global.__extensionFactoryCacheTest; +} + +function resetState(): void { + delete (globalThis as typeof globalThis & { __extensionFactoryCacheTest?: TestState }).__extensionFactoryCacheTest; +} + +function writeCountingExtension(filePath: string): void { + writeFileSync( + filePath, + ` +const state = (globalThis.__extensionFactoryCacheTest ??= {}); +state.moduleLoads = (state.moduleLoads ?? 0) + 1; + +export default function () { + state.factoryRuns = (state.factoryRuns ?? 0) + 1; +} +`, + "utf-8", + ); +} + +describe("extension factory cache", () => { + const roots: string[] = []; + + function fixture(name: string) { + const root = join(tmpdir(), `pi-extension-cache-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`); + const cwd = join(root, "project"); + const agentDir = join(root, "agent"); + mkdirSync(cwd, { recursive: true }); + mkdirSync(agentDir, { recursive: true }); + roots.push(root); + return { root, cwd, agentDir }; + } + + beforeEach(() => { + resetState(); + clearExtensionCache(); + }); + + afterEach(() => { + while (roots.length > 0) { + const root = roots.pop(); + if (root && existsSync(root)) { + rmSync(root, { recursive: true, force: true }); + } + } + resetState(); + clearExtensionCache(); + }); + + it("caches extension modules for cached same-cwd loads but reruns factories", async () => { + const { root, cwd } = fixture("same-cwd"); + const extensionPath = join(root, "counting.ts"); + writeCountingExtension(extensionPath); + + const first = await loadExtensionsCached([extensionPath], cwd); + const second = await loadExtensionsCached([extensionPath], cwd); + + expect(state().moduleLoads).toBe(1); + expect(state().factoryRuns).toBe(2); + expect(first.extensions[0]).not.toBe(second.extensions[0]); + expect(first.runtime).not.toBe(second.runtime); + }); + + it("does not cache direct loadExtensions calls", async () => { + const { root, cwd } = fixture("direct"); + const extensionPath = join(root, "counting.ts"); + writeCountingExtension(extensionPath); + + await loadExtensions([extensionPath], cwd); + await loadExtensions([extensionPath], cwd); + + expect(state().moduleLoads).toBe(2); + expect(state().factoryRuns).toBe(2); + }); + + it("clears the cache on resource loader reload", async () => { + const { cwd, agentDir } = fixture("reload"); + const extensionDir = join(agentDir, "extensions"); + mkdirSync(extensionDir, { recursive: true }); + writeCountingExtension(join(extensionDir, "counting.ts")); + const loader = new DefaultResourceLoader({ + cwd, + agentDir, + noSkills: true, + noPromptTemplates: true, + noThemes: true, + }); + + await loader.reload(); + await loader.reload(); + + expect(state().moduleLoads).toBe(2); + expect(state().factoryRuns).toBe(2); + }); + + it("keeps the cache scoped to one cwd", async () => { + const { root } = fixture("cross-cwd"); + const firstCwd = join(root, "first"); + const secondCwd = join(root, "second"); + mkdirSync(firstCwd, { recursive: true }); + mkdirSync(secondCwd, { recursive: true }); + const extensionPath = join(root, "counting.ts"); + writeCountingExtension(extensionPath); + + await loadExtensionsCached([extensionPath], firstCwd); + await loadExtensionsCached([extensionPath], secondCwd); + await loadExtensionsCached([extensionPath], secondCwd); + + expect(state().moduleLoads).toBe(2); + expect(state().factoryRuns).toBe(3); + }); +}); diff --git a/packages/coding-agent/test/theme-detection.test.ts b/packages/coding-agent/test/theme-detection.test.ts index 4ad0cf56..6bdc597b 100644 --- a/packages/coding-agent/test/theme-detection.test.ts +++ b/packages/coding-agent/test/theme-detection.test.ts @@ -5,6 +5,8 @@ import { detectTerminalBackgroundTheme, getThemeByName, getThemeForRgbColor, + parseAutoThemeSetting, + resolveThemeSetting, } from "../src/modes/interactive/theme/theme.ts"; afterEach(() => { @@ -119,3 +121,13 @@ describe("theme detection from RGB", () => { expect(getThemeForRgbColor({ r: 250, g: 250, b: 250 })).toBe("light"); }); }); + +describe("theme setting helpers", () => { + it("parses and resolves automatic theme settings", () => { + expect(parseAutoThemeSetting("light/dark")).toEqual({ lightTheme: "light", darkTheme: "dark" }); + expect(resolveThemeSetting("dark", "light")).toBe("dark"); + expect(resolveThemeSetting("light/dark", "light")).toBe("light"); + expect(resolveThemeSetting("light/dark", "dark")).toBe("dark"); + expect(resolveThemeSetting("light/dark/extra", "dark")).toBeUndefined(); + }); +}); diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index c5e8fa6f..f8b09c90 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -44,6 +44,7 @@ describe("Coding Agent Tools", () => { }); afterEach(() => { + vi.restoreAllMocks(); // Clean up test directory rmSync(testDir, { recursive: true, force: true }); }); @@ -535,6 +536,56 @@ describe("Coding Agent Tools", () => { expect(getShellConfigSpy).toHaveBeenCalledWith("/custom/bash"); }); + it("should send commands over stdin when shell resolution requires it", async () => { + vi.spyOn(shellModule, "getShellConfig").mockReturnValue({ + shell: process.execPath, + args: [ + "-e", + 'let input = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { input += chunk; }); process.stdin.on("end", () => { process.stdout.write(input); });', + ], + commandTransport: "stdin", + }); + const chunks: Buffer[] = []; + const ops = createLocalBashOperations({ shellPath: "C:\\Windows\\System32\\bash.exe" }); + const nameExpansion = "$" + "{name}"; + const countExpansion = "$" + "{count}"; + const iExpansion = "$" + "{i}"; + const command = `name='World'; echo "Hello, ${nameExpansion}!"; count=3; for i in $(seq 1 ${countExpansion}); do echo "Iteration ${iExpansion} of ${countExpansion}"; done`; + + const result = await ops.exec(command, testDir, { + onData: (data) => chunks.push(data), + }); + + expect(result.exitCode).toBe(0); + expect(Buffer.concat(chunks).toString("utf-8")).toBe(command); + }); + + it("should resolve legacy WSL bash.exe to stdin command transport", () => { + if (process.platform === "win32") return; + const originalCwd = process.cwd(); + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + const shellPath = "C:\\Windows\\System32\\bash.exe"; + writeFileSync(join(testDir, shellPath), ""); + try { + process.chdir(testDir); + Object.defineProperty(process, "platform", { + configurable: true, + value: "win32", + }); + + expect(shellModule.getShellConfig(shellPath)).toEqual({ + shell: shellPath, + args: ["-s"], + commandTransport: "stdin", + }); + } finally { + process.chdir(originalCwd); + if (platformDescriptor) { + Object.defineProperty(process, "platform", platformDescriptor); + } + } + }); + it("should prepend command prefix when configured", async () => { const bashWithPrefix = createBashTool(testDir, { commandPrefix: "export TEST_VAR=hello", @@ -980,6 +1031,57 @@ describe("edit tool fuzzy matching", () => { expect(readFileSync(testFile, "utf-8")).toBe("console.log('world');\nhello universe\n"); }); + + it("should preserve the correct occurrence when fuzzy replacement equals a nearby line", async () => { + const testFile = join(testDir, "fuzzy-preserve-duplicate-line.txt"); + const originalContent = ["replace me\u0020\u0020\u0020", "after\u0020\u0020\u0020", ""].join("\n"); + writeFileSync(testFile, originalContent); + + const result = await editTool.execute("test-fuzzy-preserve-duplicate-line", { + path: testFile, + edits: [{ oldText: "replace me\n", newText: "after\n" }], + }); + + const expectedContent = ["after", "after\u0020\u0020\u0020", ""].join("\n"); + expect(readFileSync(testFile, "utf-8")).toBe(expectedContent); + expect(applyPatch(originalContent, result.details?.patch ?? "")).toBe(expectedContent); + }); + + it("should preserve untouched lines and produce an applicable patch for fuzzy multi-edits", async () => { + const testFile = join(testDir, "fuzzy-preserve-multi.txt"); + const originalContent = [ + "keep before\u0020\u0020", + "first target\u0020\u0020", + "first after", + "keep middle\u0020\u0020\u0020", + "second target\u0020\u0020", + "second after", + "keep after\u0020\u0020", + "", + ].join("\n"); + writeFileSync(testFile, originalContent); + + const result = await editTool.execute("test-fuzzy-preserve-multi", { + path: testFile, + edits: [ + { oldText: "first target\nfirst after", newText: "FIRST\nFIRST2" }, + { oldText: "second target\nsecond after", newText: "SECOND\nSECOND2" }, + ], + }); + + const expectedContent = [ + "keep before\u0020\u0020", + "FIRST", + "FIRST2", + "keep middle\u0020\u0020\u0020", + "SECOND", + "SECOND2", + "keep after\u0020\u0020", + "", + ].join("\n"); + expect(readFileSync(testFile, "utf-8")).toBe(expectedContent); + expect(applyPatch(originalContent, result.details?.patch ?? "")).toBe(expectedContent); + }); }); describe("edit tool CRLF handling", () => { diff --git a/packages/coding-agent/test/utilities.ts b/packages/coding-agent/test/utilities.ts index 5ebf6813..73177ce9 100644 --- a/packages/coding-agent/test/utilities.ts +++ b/packages/coding-agent/test/utilities.ts @@ -6,7 +6,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } import { homedir, tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { getModel, type OAuthCredentials, type OAuthProvider } from "@earendil-works/pi-ai"; +import { getModel, type OAuthCredentials, type OAuthProvider } from "@earendil-works/pi-ai/compat"; import { getOAuthApiKey } from "@earendil-works/pi-ai/oauth"; import { AgentSession } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts index 67ce0fca..fe12421c 100644 --- a/packages/coding-agent/vitest.config.ts +++ b/packages/coding-agent/vitest.config.ts @@ -2,6 +2,7 @@ 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)); const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url)); const agentSrcIndex = fileURLToPath(new URL("../agent/src/index.ts", import.meta.url)); const tuiSrcIndex = fileURLToPath(new URL("../tui/src/index.ts", import.meta.url)); @@ -20,6 +21,7 @@ export default defineConfig({ resolve: { alias: [ { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex }, + { find: /^@earendil-works\/pi-ai\/compat$/, replacement: aiSrcCompat }, { find: /^@earendil-works\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, { find: /^@earendil-works\/pi-agent-core$/, replacement: agentSrcIndex }, { find: /^@earendil-works\/pi-tui$/, replacement: tuiSrcIndex }, diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 9b90a5b1..2823c818 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,8 +2,32 @@ ## [Unreleased] +## [0.80.2] - 2026-06-23 + +## [0.80.1] - 2026-06-23 + +## [0.80.0] - 2026-06-23 + +### Changed + +- Added `Ctrl+J` as a default newline keybinding alongside `Shift+Enter`. + +## [0.79.10] - 2026-06-22 + +## [0.79.9] - 2026-06-20 + +### Fixed + +- Fixed Markdown streaming code fence rendering so partial closing fences no longer make code blocks shrink or flicker while content streams ([#5846](https://github.com/earendil-works/pi/pull/5846) by [@xl0](https://github.com/xl0)). + +## [0.79.8] - 2026-06-19 + +## [0.79.7] - 2026-06-18 + ### Added +- Added terminal color-scheme query and notification support for light/dark appearance detection (`TUI.queryTerminalColorScheme()`, `TUI.onTerminalColorSchemeChange()`, and `TUI.setTerminalColorSchemeNotifications()`) ([#5874](https://github.com/earendil-works/pi/pull/5874)). +- Added Warp terminal detection for Kitty graphics inline image support ([#5841](https://github.com/earendil-works/pi/pull/5841) by [@dodiego](https://github.com/dodiego)). - Exported `sliceByColumn` for ANSI-aware horizontal column slicing. ## [0.79.6] - 2026-06-16 diff --git a/packages/tui/package.json b/packages/tui/package.json index 9c73829c..e407a72d 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.6", + "version": "0.80.2", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 83f15aa5..1034cb47 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -22,6 +22,31 @@ class StrictStrikethroughTokenizer extends Tokenizer { } } +function trimPartialClosingFences(tokens: readonly Token[]): void { + const token = tokens[tokens.length - 1]; + if (token?.type === "list") { + trimPartialClosingFences(token.items[token.items.length - 1]?.tokens ?? []); + return; + } + if (token?.type === "blockquote") { + trimPartialClosingFences(token.tokens ?? []); + return; + } + if (token?.type !== "code") { + return; + } + + // Trim streamed partial closing fences so code blocks do not shrink/flicker + // when the final fence character arrives. See https://github.com/earendil-works/pi/issues/5825. + const marker = /^(`{3,}|~{3,})/.exec(token.raw)?.[1]; + const lastLine = token.raw.split("\n").pop(); + if (!marker || !lastLine || lastLine.length >= marker.length || lastLine !== marker[0]?.repeat(lastLine.length)) { + return; + } + + token.text = token.text.slice(0, -lastLine.length).replace(/\n$/, ""); +} + const markdownParser = new Marked(); markdownParser.setOptions({ tokenizer: new StrictStrikethroughTokenizer(), @@ -145,6 +170,7 @@ export class Markdown implements Component { // Parse markdown to HTML-like tokens const tokens = markdownParser.lexer(normalizedText); + trimPartialClosingFences(tokens); // Convert tokens to styled terminal output const renderedLines: string[] = []; diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index e22bf08a..4e76b107 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -62,7 +62,12 @@ export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from " // Terminal interface and implementations export { ProcessTerminal, type Terminal } from "./terminal.ts"; // Terminal colors -export { parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts"; +export { + parseOsc11BackgroundColor, + parseTerminalColorSchemeReport, + type RgbColor, + type TerminalColorScheme, +} from "./terminal-colors.ts"; // Terminal image support export { allocateImageId, diff --git a/packages/tui/src/keybindings.ts b/packages/tui/src/keybindings.ts index 789124ad..eaf0838b 100644 --- a/packages/tui/src/keybindings.ts +++ b/packages/tui/src/keybindings.ts @@ -115,7 +115,7 @@ export const TUI_KEYBINDINGS = { "tui.editor.yank": { defaultKeys: "ctrl+y", description: "Yank" }, "tui.editor.yankPop": { defaultKeys: "alt+y", description: "Yank pop" }, "tui.editor.undo": { defaultKeys: "ctrl+-", description: "Undo" }, - "tui.input.newLine": { defaultKeys: "shift+enter", description: "Insert newline" }, + "tui.input.newLine": { defaultKeys: ["shift+enter", "ctrl+j"], description: "Insert newline" }, "tui.input.submit": { defaultKeys: "enter", description: "Submit input" }, "tui.input.tab": { defaultKeys: "tab", description: "Tab / autocomplete" }, "tui.input.copy": { defaultKeys: "ctrl+c", description: "Copy selection" }, diff --git a/packages/tui/src/terminal-colors.ts b/packages/tui/src/terminal-colors.ts index 1c383a51..cff6dc8e 100644 --- a/packages/tui/src/terminal-colors.ts +++ b/packages/tui/src/terminal-colors.ts @@ -4,6 +4,8 @@ export interface RgbColor { b: number; } +export type TerminalColorScheme = "dark" | "light"; + function hexToRgb(hex: string): RgbColor { const normalized = hex.startsWith("#") ? hex.slice(1) : hex; const r = parseInt(normalized.slice(0, 2), 16); @@ -24,6 +26,7 @@ function parseOscHexChannel(channel: string): number | undefined { } const OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN = /^\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)$/i; +const COLOR_SCHEME_REPORT_PATTERN = /^\x1b\[\?997;(1|2)n$/; export function isOsc11BackgroundColorResponse(data: string): boolean { return OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN.test(data); @@ -60,3 +63,11 @@ export function parseOsc11BackgroundColor(data: string): RgbColor | undefined { const b = parseOscHexChannel(blue); return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; } + +export function parseTerminalColorSchemeReport(data: string): TerminalColorScheme | undefined { + const match = data.match(COLOR_SCHEME_REPORT_PATTERN); + if (!match) { + return undefined; + } + return match[1] === "2" ? "light" : "dark"; +} diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 1fff6545..a7054888 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -8,7 +8,13 @@ import * as path from "node:path"; import { performance } from "node:perf_hooks"; import { isKeyRelease, matchesKey } from "./keys.ts"; import type { Terminal } from "./terminal.ts"; -import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts"; +import { + isOsc11BackgroundColorResponse, + parseOsc11BackgroundColor, + parseTerminalColorSchemeReport, + type RgbColor, + type TerminalColorScheme, +} from "./terminal-colors.ts"; import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.ts"; import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.ts"; @@ -311,6 +317,8 @@ export class TUI extends Container { private stopped = false; private pendingOsc11BackgroundReplies = 0; private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = []; + private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>(); + private terminalColorSchemeNotificationsEnabled = false; // Overlay stack for modal components rendered on top of base content private focusOrderCounter = 0; @@ -631,6 +639,9 @@ export class TUI extends Container { () => this.requestRender(), ); this.terminal.hideCursor(); + if (this.terminalColorSchemeNotificationsEnabled) { + this.terminal.write("\x1b[?2031h"); + } this.queryCellSize(); this.requestRender(); } @@ -646,6 +657,23 @@ export class TUI extends Container { this.inputListeners.delete(listener); } + onTerminalColorSchemeChange(listener: (scheme: TerminalColorScheme) => void): () => void { + this.terminalColorSchemeListeners.add(listener); + return () => { + this.terminalColorSchemeListeners.delete(listener); + }; + } + + setTerminalColorSchemeNotifications(enabled: boolean): void { + if (this.terminalColorSchemeNotificationsEnabled === enabled) { + return; + } + this.terminalColorSchemeNotificationsEnabled = enabled; + if (!this.stopped) { + this.terminal.write(enabled ? "\x1b[?2031h" : "\x1b[?2031l"); + } + } + private queryCellSize(): void { // Only query if terminal supports images (cell size is only used for image rendering) if (!getCapabilities().images) { @@ -662,6 +690,9 @@ export class TUI extends Container { clearTimeout(this.renderTimer); this.renderTimer = undefined; } + if (this.terminalColorSchemeNotificationsEnabled) { + this.terminal.write("\x1b[?2031l"); + } // Move cursor to the end of the content to prevent overwriting/artifacts on exit if (this.previousLines.length > 0) { const targetRow = this.previousLines.length; // Line after the last content @@ -731,6 +762,9 @@ export class TUI extends Container { if (this.consumeOsc11BackgroundResponse(data)) { return; } + if (this.consumeTerminalColorSchemeReport(data)) { + return; + } if (this.inputListeners.size > 0) { let current = data; @@ -824,6 +858,18 @@ export class TUI extends Container { return true; } + private consumeTerminalColorSchemeReport(data: string): boolean { + const scheme = parseTerminalColorSchemeReport(data); + if (!scheme) { + return false; + } + + for (const listener of this.terminalColorSchemeListeners) { + listener(scheme); + } + return true; + } + private consumeCellSizeResponse(data: string): boolean { // Response format: ESC [ 6 ; height ; width t const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/); @@ -1638,4 +1684,31 @@ export class TUI extends Container { this.terminal.write("\x1b]11;?\x07"); }); } + + /** + * Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`). + * Terminals that support the color palette notification protocol reply with + * `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light. + */ + queryTerminalColorScheme({ timeoutMs }: { timeoutMs: number }): Promise { + return new Promise((resolve) => { + let settled = false; + let timer: NodeJS.Timeout | undefined; + let unsubscribe: () => void = () => {}; + const settle = (scheme: TerminalColorScheme | undefined) => { + if (settled) return; + settled = true; + if (timer) { + clearTimeout(timer); + timer = undefined; + } + unsubscribe(); + resolve(scheme); + }; + + unsubscribe = this.onTerminalColorSchemeChange(settle); + timer = setTimeout(() => settle(undefined), timeoutMs); + this.terminal.write("\x1b[?996n"); + }); + } } diff --git a/packages/tui/test/keybindings.test.ts b/packages/tui/test/keybindings.test.ts index 37c67442..8bd7f438 100644 --- a/packages/tui/test/keybindings.test.ts +++ b/packages/tui/test/keybindings.test.ts @@ -3,6 +3,14 @@ import { describe, it } from "node:test"; import { KeybindingsManager, TUI_KEYBINDINGS } from "../src/keybindings.ts"; describe("KeybindingsManager", () => { + it("binds Ctrl+J as a default newline alias", () => { + const keybindings = new KeybindingsManager(TUI_KEYBINDINGS); + + assert.deepStrictEqual(keybindings.getKeys("tui.input.newLine"), ["shift+enter", "ctrl+j"]); + assert.strictEqual(keybindings.matches("\n", "tui.input.newLine"), true); + assert.strictEqual(keybindings.matches("\x1b[106;5u", "tui.input.newLine"), true); + }); + it("does not evict selector confirm when input submit is rebound", () => { const keybindings = new KeybindingsManager(TUI_KEYBINDINGS, { "tui.input.submit": ["enter", "ctrl+enter"], diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index dc595b11..47bc0a81 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -1376,4 +1376,47 @@ bar`, ); }); }); + + describe("Streaming code fences", () => { + it("stabilizes partial closing fence rendering", () => { + const cases = [ + { + input: "```ts\nconst x = 1;\n``", + expected: ["```ts", " const x = 1;", "```"], + }, + { + input: "```md\nnot a closing fence:\n``\n```", + expected: ["```md", " not a closing fence:", " ``", "```"], + }, + { + input: "```ts\n``", + expected: ["```ts", "", "```"], + }, + { + input: "````\n```", + expected: ["```", "", "```"], + }, + { + input: "~~~~~\n~~~~", + expected: ["```", "", "```"], + }, + { + input: "```md\nnot a closing fence:\n``\n```\n\nafter", + expected: ["```md", " not a closing fence:", " ``", "```", "", "after"], + }, + ]; + + for (const { input, expected } of cases) { + const markdown = new Markdown(input, 0, 0, defaultMarkdownTheme); + const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd()); + + assert.deepStrictEqual(lines, expected); + } + + const partial = new Markdown("```ts\nconst x = 1;\n``", 0, 0, defaultMarkdownTheme); + const complete = new Markdown("```ts\nconst x = 1;\n```", 0, 0, defaultMarkdownTheme); + + assert.strictEqual(partial.render(80).length, complete.render(80).length); + }); + }); }); diff --git a/packages/tui/test/terminal-colors.test.ts b/packages/tui/test/terminal-colors.test.ts index 9ac2a4ee..d777e061 100644 --- a/packages/tui/test/terminal-colors.test.ts +++ b/packages/tui/test/terminal-colors.test.ts @@ -1,6 +1,12 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { type Component, parseOsc11BackgroundColor, type Terminal, TUI } from "../src/index.ts"; +import { + type Component, + parseOsc11BackgroundColor, + parseTerminalColorSchemeReport, + type Terminal, + TUI, +} from "../src/index.ts"; class TestTerminal implements Terminal { private inputHandler?: (data: string) => void; @@ -104,6 +110,16 @@ describe("parseOsc11BackgroundColor", () => { }); }); +describe("parseTerminalColorSchemeReport", () => { + it("parses color scheme reports", () => { + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;1n"), "dark"); + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;2n"), "light"); + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;3n"), undefined); + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?996n"), undefined); + assert.strictEqual(parseTerminalColorSchemeReport("x\x1b[?997;1n"), undefined); + }); +}); + describe("TUI.queryTerminalBackgroundColor", () => { it("writes OSC 11 query and resolves with the parsed RGB reply", async () => { const terminal = new TestTerminal(); diff --git a/scripts/browser-smoke-entry.ts b/scripts/browser-smoke-entry.ts index 066a6099..3ac7bcf0 100644 --- a/scripts/browser-smoke-entry.ts +++ b/scripts/browser-smoke-entry.ts @@ -1,4 +1,5 @@ -import { complete, createAssistantMessageEventStream, getModel, getProviders, Type } from "@earendil-works/pi-ai"; +import { createAssistantMessageEventStream, Type } from "@earendil-works/pi-ai"; +import { complete, getModel, getProviders } from "@earendil-works/pi-ai/compat"; import { Agent, bashExecutionToText, diff --git a/scripts/generate-coding-agent-shrinkwrap.mjs b/scripts/generate-coding-agent-shrinkwrap.mjs index 7bcf5681..1971869c 100644 --- a/scripts/generate-coding-agent-shrinkwrap.mjs +++ b/scripts/generate-coding-agent-shrinkwrap.mjs @@ -12,7 +12,7 @@ const shrinkwrapPath = join(codingAgentDir, "npm-shrinkwrap.json"); const internalPackagePrefix = "@earendil-works/pi-"; const allowedInstallScriptPackages = new Map([ ["@google/genai@1.52.0", "preinstall is a no-op in the published package"], - ["protobufjs@7.5.9", "postinstall only warns about protobufjs version scheme mismatches"], + ["protobufjs@7.6.4", "postinstall only warns about protobufjs version scheme mismatches"], ]); const args = new Set(process.argv.slice(2)); diff --git a/scripts/publish.mjs b/scripts/publish.mjs index 513ab36f..920954f8 100644 --- a/scripts/publish.mjs +++ b/scripts/publish.mjs @@ -89,24 +89,34 @@ if (versions.length !== 1) { console.log(`Publishing pi packages at ${versions[0]}${dryRun ? " (dry run)" : ""}\n`); -for (const pkg of packages) { - const version = packageVersions.get(pkg.name); +const packageStates = packages.map((pkg) => ({ + ...pkg, + published: false, + version: packageVersions.get(pkg.name), +})); + +for (const pkg of packageStates) { assertBuildOutputExists(pkg.directory); - const published = isPublished(pkg.name, version); + pkg.published = isPublished(pkg.name, pkg.version); - if (dryRun) { - if (published) { - console.log(`${pkg.name}@${version} is already published; validating package contents only.`); - } else { - console.log(`${pkg.name}@${version} is not published; validating package contents before publish.`); - } - validatePack(pkg.directory); - console.log(); - continue; + if (pkg.published) { + console.log(`${pkg.name}@${pkg.version} is already published; validating package contents only.`); + } else { + console.log(`${pkg.name}@${pkg.version} is not published; validating package contents before publish.`); } + validatePack(pkg.directory); + console.log(); +} - if (published) { - console.log(`Skipping ${pkg.name}@${version}: already published\n`); +if (dryRun) { + process.exit(0); +} + +console.log("All packages validated; starting publication.\n"); + +for (const pkg of packageStates) { + if (pkg.published) { + console.log(`Skipping ${pkg.name}@${pkg.version}: already published\n`); continue; } diff --git a/scripts/repro-5893-wsl-bash.mjs b/scripts/repro-5893-wsl-bash.mjs new file mode 100644 index 00000000..b5048de7 --- /dev/null +++ b/scripts/repro-5893-wsl-bash.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node +import { existsSync } from "node:fs"; +import { createBashTool } from "../packages/coding-agent/src/core/tools/bash.ts"; + +const shellPath = "C:\\Windows\\System32\\bash.exe"; +const nameExpansion = "$" + "{name}"; +const countExpansion = "$" + "{count}"; +const iExpansion = "$" + "{i}"; + +function getTextOutput(result) { + return result.content + .filter((content) => content.type === "text") + .map((content) => content.text ?? "") + .join("\n"); +} + +async function runCase(label, command, expectedOutput) { + const tool = createBashTool(process.cwd(), { shellPath }); + const result = await tool.execute(label, { command }); + const output = getTextOutput(result).trimEnd(); + if (output !== expectedOutput) { + throw new Error( + [ + `${label} failed`, + "Expected:", + expectedOutput, + "Actual:", + output, + ].join("\n"), + ); + } + console.log(output); +} + +if (process.platform !== "win32") { + throw new Error("This repro must run from Windows PowerShell/CMD, not macOS/Linux or inside WSL."); +} + +if (!existsSync(shellPath)) { + throw new Error(`WSL bash launcher not found at ${shellPath}. Install/enable WSL first.`); +} + +await runCase( + "issue-5893-simple-variable", + `name='World'; echo "Hello, ${nameExpansion}!"`, + "Hello, World!", +); + +await runCase( + "issue-5893-loop-variable", + `count=3; for i in $(seq 1 ${countExpansion}); do echo "Iteration ${iExpansion} of ${countExpansion}"; done`, + "Iteration 1 of 3\nIteration 2 of 3\nIteration 3 of 3", +); + +console.log("issue #5893 WSL bash repro passed"); diff --git a/test.sh b/test.sh index 9a553f6f..b4ea70c7 100755 --- a/test.sh +++ b/test.sh @@ -25,6 +25,8 @@ export PI_NO_LOCAL_LLM=1 # Unset API keys (see packages/ai/src/stream.ts getEnvApiKey) unset ANTHROPIC_API_KEY unset ANTHROPIC_OAUTH_TOKEN +unset ANT_LING_API_KEY +unset NVIDIA_API_KEY unset OPENAI_API_KEY unset AZURE_OPENAI_API_KEY unset DEEPSEEK_API_KEY