mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-16 11:20:42 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54fea4b111 | ||
|
|
4a5fa79461 |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Preserve prompt cache prefixes when sessions move between locations with unchanged instructions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Fix OpenCode Console device authorization URLs when the server returns an origin-rooted verification path.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Prompt and synthetic inbox ID reuse is now idempotent: reusing an ID within the same Session succeeds and returns the first admission, ignoring the retried payload, metadata, and delivery mode. Previously reuse with a differing payload failed with a conflict. Cross-Session and cross-type reuse still fail, and control items keep their operation-specific conflict behavior.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Nested AGENTS.md instructions are re-injected after compaction. Previously the in-memory dedup claim outlived the synthetic message that compaction dropped from model-visible history, so nested instructions were silently lost for the rest of the process lifetime. The claim now only guards in-flight loads; the synthetic message metadata in durable history is the sole lasting ledger, so any history truncation (compaction, revert) self-heals on the next read in that subtree.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/plugin": patch
|
||||
---
|
||||
|
||||
Derive Promise plugin API request and response conversion from the canonical protocol schemas.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@opencode-ai/core": minor
|
||||
"@opencode-ai/schema": minor
|
||||
"@opencode-ai/protocol": minor
|
||||
"@opencode-ai/client": minor
|
||||
---
|
||||
|
||||
Remove the unused question request API and use session forms for question tool interactions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Apply shared Session model-request preparation to transient generation.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@opencode-ai/core": minor
|
||||
"@opencode-ai/schema": minor
|
||||
"@opencode-ai/protocol": minor
|
||||
"@opencode-ai/client": minor
|
||||
---
|
||||
|
||||
Add an opt-in portable shell permission scanner. Opaque commands use normal shell authorization without inferring
|
||||
external directories, while the default tree-sitter path remains unchanged.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Make the experimental portable shell scanner authoritative, with no Tree-sitter
|
||||
fallback. Scan common Bash and PowerShell control flow, heredocs, functions,
|
||||
expressions, quoting, and substitutions natively. Preserve existing redirect and
|
||||
declaration permission matching, and make PowerShell saved approvals cover the
|
||||
original command spelling. Parser failures remain visible without changing the
|
||||
permission engine. The default Tree-sitter path is unchanged.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Simplify interrupt continuation: the steer-scoped resume decision now lives in SessionExecution as a post-cleanup inbox check, and the run coordinator drops its continuation state machine. Wakes arriving during cancellation cleanup now restart a normal full drain, and interrupting an idle session with continue now resumes pending steering input. Recovery-applied moves now end with the same full wake as inbox-admitted moves, retrying any stranded inbox work at the new location. Interrupting with continue now also resumes a next-in-line control item: between-turn manual compaction and moves run under any drain scope, while queued prompts remain parked.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@opencode-ai/client": patch
|
||||
"@opencode-ai/plugin": patch
|
||||
---
|
||||
|
||||
Add form reply and cancellation operations that reconcile terminal forms in the local TUI projection.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Title generation and compaction summaries now build their model requests through the shared session request boundary, gaining unsupported-media filtering and image bounds while explicitly opting out of session context hooks: plugins that shape the agent conversation do not observe title or compaction requests. Title requests gain the fork-aware session prompt cache key, and compaction summaries in forked sessions reuse the fork root's prompt cache key instead of the fork's own.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@opencode-ai/plugin": patch
|
||||
"@opencode-ai/core": patch
|
||||
---
|
||||
|
||||
Add transport-neutral Session model request hooks and provider-scoped hook registration so eligible OpenAI Responses requests can prefer WebSocket without bypassing HTTP-only middleware.
|
||||
@@ -2,4 +2,3 @@ packages/core/migration/**/snapshot.json linguist-generated
|
||||
packages/core/src/database/migration.gen.ts linguist-generated
|
||||
packages/core/src/models-dev/snapshot.txt linguist-generated
|
||||
packages/core/src/**/*.txt text eol=lf
|
||||
packages/httpapi-codegen/test/generated/*.ts text eol=lf
|
||||
@@ -45,16 +45,16 @@ runs:
|
||||
- name: Get cache directory
|
||||
id: cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$(bun --version)" >> "$GITHUB_OUTPUT"
|
||||
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore Bun dependencies
|
||||
id: bun-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ steps.cache.outputs.version }}-${{ hashFiles('bun.lock', 'patches/**') }}
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install setuptools for distutils compatibility
|
||||
run: python3 -m pip install setuptools || pip install setuptools || true
|
||||
@@ -66,9 +66,9 @@ runs:
|
||||
# e.g. ./patches/ for standard-openapi
|
||||
# https://github.com/oven-sh/bun/issues/28147
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||
bun install --frozen-lockfile --linker hoisted ${{ inputs.install-flags }}
|
||||
bun install --linker hoisted ${{ inputs.install-flags }}
|
||||
else
|
||||
bun install --frozen-lockfile ${{ inputs.install-flags }}
|
||||
bun install ${{ inputs.install-flags }}
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -77,4 +77,4 @@ runs:
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.cache.outputs.dir }}
|
||||
key: ${{ steps.bun-cache.outputs.cache-primary-key }}
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
@@ -1,37 +0,0 @@
|
||||
name: check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ case(github.ref == 'refs/heads/dev', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: typecheck
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
# A pull request checks out its merge into the current base; the first parent is that base.
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run checks
|
||||
run: bun run check
|
||||
|
||||
# Every GUI package file (app, desktop, gui-extensions, ui, session-ui) a pull request adds or edits must be free of
|
||||
# oxlint problems, warn-level rules (anti-slop) included. Other packages are not affected.
|
||||
- name: Lint changed files
|
||||
if: github.event_name == 'pull_request'
|
||||
# Against the merge's first parent, so base-branch commits the pull request has not merged are not counted as its
|
||||
# changes (the event's base SHA can predate them).
|
||||
run: bun run lint:changed HEAD^1
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
close-non-compliant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close non-compliant issues and PRs after 72 hours
|
||||
- name: Close non-compliant issues and PRs after 2 hours
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const seventyTwoHours = 72 * 60 * 60 * 1000;
|
||||
const twoHours = 2 * 60 * 60 * 1000;
|
||||
const orgMemberAssociations = new Set(['OWNER', 'MEMBER']);
|
||||
const agentLogin = 'opencode-agent[bot]';
|
||||
const { data: file } = await github.rest.repos.getContent({
|
||||
@@ -87,14 +87,14 @@ jobs:
|
||||
if (!complianceComment) continue;
|
||||
|
||||
const commentAge = now - new Date(complianceComment.created_at).getTime();
|
||||
if (commentAge < seventyTwoHours) {
|
||||
core.info(`${kind} #${item.number} still within 72-hour window (${Math.round(commentAge / 60000)}m elapsed)`);
|
||||
if (commentAge < twoHours) {
|
||||
core.info(`${kind} #${item.number} still within 2-hour window (${Math.round(commentAge / 60000)}m elapsed)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const closeMessage = isPR
|
||||
? 'This pull request has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 72-hour window.\n\nFeel free to open a new pull request that follows our guidelines.'
|
||||
: 'This issue has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 72-hour window.\n\nFeel free to open a new issue that follows our issue templates.';
|
||||
? 'This pull request has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new pull request that follows our guidelines.'
|
||||
: 'This issue has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new issue that follows our issue templates.';
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
@@ -129,5 +129,5 @@ jobs:
|
||||
});
|
||||
}
|
||||
|
||||
core.info(`Closed non-compliant ${kind} #${item.number} after 72-hour window`);
|
||||
core.info(`Closed non-compliant ${kind} #${item.number} after 2-hour window`);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
name: deploy-files
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- production
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-files-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Typecheck
|
||||
working-directory: services/files
|
||||
run: bun typecheck
|
||||
|
||||
- name: Deploy
|
||||
working-directory: services/files
|
||||
run: bun run deploy --env ${{ github.ref_name == 'production' && 'production' || 'dev' }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -3,7 +3,7 @@ name: deploy-posts
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
- v2
|
||||
paths:
|
||||
- packages/posts/**
|
||||
- bun.lock
|
||||
@@ -19,7 +19,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name == 'production'
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name == 'v2'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
name: deploy-update
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-update-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name == 'production'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Typecheck
|
||||
working-directory: services/update
|
||||
run: bun typecheck
|
||||
|
||||
- name: Deploy
|
||||
working-directory: services/update
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- production
|
||||
- v2
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -16,7 +16,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'v2')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
@@ -24,13 +24,13 @@ jobs:
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Build
|
||||
working-directory: services/www
|
||||
working-directory: packages/www
|
||||
run: bun run build
|
||||
env:
|
||||
CLOUDFLARE_ENV: ${{ github.ref_name == 'production' && 'production' || 'dev' }}
|
||||
CLOUDFLARE_ENV: ${{ github.ref_name == 'v2' && 'production' || 'dev' }}
|
||||
|
||||
- name: Deploy
|
||||
working-directory: services/www
|
||||
working-directory: packages/www
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -5,7 +5,6 @@ on:
|
||||
branches:
|
||||
- dev
|
||||
- production
|
||||
- beta
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -16,8 +15,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production' || github.ref_name == 'beta')
|
||||
runs-on: ${{ github.ref_name == 'dev' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.ref_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
@@ -29,7 +28,6 @@ jobs:
|
||||
node-version: "24"
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
|
||||
if: github.ref_name != 'beta'
|
||||
with:
|
||||
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
|
||||
role-session-name: opencode-${{ github.run_id }}
|
||||
@@ -37,7 +35,6 @@ jobs:
|
||||
|
||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
|
||||
If the issue is NOT compliant and the author association is not OWNER or MEMBER, start the comment with:
|
||||
<!-- issue-compliance -->
|
||||
Then explain what needs to be fixed and that they have 72 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance
|
||||
Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance
|
||||
|
||||
If duplicates were found, include a section about potential duplicates with links.
|
||||
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
**What needs to be fixed:**
|
||||
- [specific reasons]
|
||||
|
||||
Please edit this issue to address the above within **72 hours**, or it will be automatically closed.
|
||||
Please edit this issue to address the above within **2 hours**, or it will be automatically closed.
|
||||
|
||||
[If duplicates found, add:]
|
||||
---
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
echo "=== Flake structure ==="
|
||||
nix flake show --all-systems
|
||||
|
||||
SYSTEMS="x86_64-linux aarch64-linux aarch64-darwin"
|
||||
SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin"
|
||||
PACKAGES="opencode"
|
||||
# TODO: move 'desktop' to PACKAGES when #11755 is fixed
|
||||
OPTIONAL_PACKAGES="desktop"
|
||||
|
||||
@@ -6,12 +6,11 @@ permissions:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [dev, beta]
|
||||
branches: [dev, beta, v2]
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
- "services/*/package.json"
|
||||
- "flake.lock"
|
||||
- "nix/node_modules.nix"
|
||||
- "nix/scripts/**"
|
||||
@@ -34,6 +33,8 @@ jobs:
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- system: aarch64-linux
|
||||
runner: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- system: x86_64-darwin
|
||||
runner: macos-15-intel
|
||||
- system: aarch64-darwin
|
||||
runner: macos-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
@@ -124,7 +125,7 @@ jobs:
|
||||
|
||||
[ -f "$HASH_FILE" ] || echo '{"nodeModules":{}}' > "$HASH_FILE"
|
||||
|
||||
for SYSTEM in x86_64-linux aarch64-linux aarch64-darwin; do
|
||||
for SYSTEM in x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin; do
|
||||
FILE="hashes/hash-${SYSTEM}/hash.txt"
|
||||
if [ -f "$FILE" ]; then
|
||||
HASH="$(tr -d '[:space:]' < "$FILE")"
|
||||
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
**What needs to be fixed:**
|
||||
${issues.map(i => `- ${i}`).join('\n')}
|
||||
|
||||
Please edit this PR description to address the above within **72 hours**, or it will be automatically closed.
|
||||
Please edit this PR description to address the above within **2 hours**, or it will be automatically closed.
|
||||
|
||||
If you believe this was flagged incorrectly, please let a maintainer know.`;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
- ci
|
||||
- dev
|
||||
- beta
|
||||
- v2
|
||||
- fix/npm-native-binary-install
|
||||
- snapshot-*
|
||||
workflow_dispatch:
|
||||
@@ -23,12 +24,8 @@ on:
|
||||
description: "Override version (optional)"
|
||||
required: false
|
||||
type: string
|
||||
release_notes:
|
||||
description: "Reviewed V2 release notes for CHANGELOG.md and the Discord announcement (optional)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ (github.ref_name == 'dev' && (inputs.version || inputs.bump) && 'release') || inputs.version || inputs.bump }}
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -36,7 +33,7 @@ permissions:
|
||||
packages: write
|
||||
|
||||
env:
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'dev' && !inputs.bump && !inputs.version && 'dev') || '' }}
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'dev') || '' }}
|
||||
|
||||
jobs:
|
||||
version:
|
||||
@@ -49,6 +46,13 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Deploy update service
|
||||
if: github.ref_name == 'v2' || github.ref_name == 'beta'
|
||||
working-directory: packages/updates
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
- name: Setup git committer
|
||||
id: committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
@@ -87,7 +91,7 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
with:
|
||||
bun-version: 1.4.2
|
||||
bun-version: 1.4.0
|
||||
|
||||
- name: Setup git committer
|
||||
id: committer
|
||||
@@ -97,7 +101,7 @@ jobs:
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Build legacy CLI
|
||||
if: github.ref_name != 'dev' && github.ref_name != 'beta'
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
run: ./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
@@ -109,14 +113,14 @@ jobs:
|
||||
id: build
|
||||
run: ./packages/cli/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
|
||||
env:
|
||||
BUN_COMPILE_RELEASE: bun-v1.4.2
|
||||
BUN_COMPILE_RELEASE: bun-v1.4.0
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||
GH_REPO: ${{ needs.version.outputs.repo }}
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: github.ref_name != 'dev' && github.ref_name != 'beta'
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli
|
||||
path: |
|
||||
@@ -124,7 +128,7 @@ jobs:
|
||||
packages/opencode/dist/opencode-linux*
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: github.ref_name != 'dev' && github.ref_name != 'beta'
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli-windows
|
||||
path: packages/opencode/dist/opencode-windows*
|
||||
@@ -164,7 +168,7 @@ jobs:
|
||||
fi
|
||||
|
||||
found=0
|
||||
for file in packages/cli/dist/cli-darwin-*/bin/opencode; do
|
||||
for file in packages/cli/dist/cli-darwin-*/bin/opencode2; do
|
||||
if [ ! -f "$file" ]; then
|
||||
continue
|
||||
fi
|
||||
@@ -187,7 +191,7 @@ jobs:
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-preview-cli-macos
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist/cli-*
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -195,7 +199,7 @@ jobs:
|
||||
needs: version
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
if: github.repository == 'anomalyco/opencode' && !(github.ref_name == 'dev' && (inputs.bump || inputs.version))
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
@@ -217,7 +221,7 @@ jobs:
|
||||
needs:
|
||||
- version
|
||||
- build-node-app-archive
|
||||
if: github.repository == 'anomalyco/opencode' && !(github.ref_name == 'dev' && (inputs.bump || inputs.version))
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -272,9 +276,10 @@ jobs:
|
||||
|
||||
sign-cli-windows:
|
||||
needs:
|
||||
- sign-cli-macos
|
||||
- build-cli
|
||||
- version
|
||||
runs-on: blacksmith-4vcpu-windows-2025
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'beta')
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
env:
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -287,8 +292,15 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-preview-cli-macos
|
||||
path: packages/cli/dist
|
||||
name: opencode-cli-windows
|
||||
path: packages/opencode/dist
|
||||
|
||||
- name: Setup git committer
|
||||
id: committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
with:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Azure login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
@@ -303,9 +315,9 @@ jobs:
|
||||
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||
files: |
|
||||
${{ github.workspace }}\packages\cli\dist\cli-windows-arm64\bin\opencode.exe
|
||||
${{ github.workspace }}\packages\cli\dist\cli-windows-x64\bin\opencode.exe
|
||||
${{ github.workspace }}\packages\cli\dist\cli-windows-x64-baseline\bin\opencode.exe
|
||||
${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe
|
||||
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe
|
||||
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe
|
||||
exclude-environment-credential: true
|
||||
exclude-workload-identity-credential: true
|
||||
exclude-managed-identity-credential: true
|
||||
@@ -321,9 +333,9 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
$files = @(
|
||||
"${{ github.workspace }}\packages\cli\dist\cli-windows-arm64\bin\opencode.exe",
|
||||
"${{ github.workspace }}\packages\cli\dist\cli-windows-x64\bin\opencode.exe",
|
||||
"${{ github.workspace }}\packages\cli\dist\cli-windows-x64-baseline\bin\opencode.exe"
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe",
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe",
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe"
|
||||
)
|
||||
|
||||
foreach ($file in $files) {
|
||||
@@ -333,18 +345,40 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
- name: Repack Windows CLI archives
|
||||
working-directory: packages/opencode/dist
|
||||
shell: pwsh
|
||||
run: |
|
||||
Compress-Archive -Path "opencode-windows-arm64\bin\*" -DestinationPath "opencode-windows-arm64.zip" -Force
|
||||
Compress-Archive -Path "opencode-windows-x64\bin\*" -DestinationPath "opencode-windows-x64.zip" -Force
|
||||
Compress-Archive -Path "opencode-windows-x64-baseline\bin\*" -DestinationPath "opencode-windows-x64-baseline.zip" -Force
|
||||
|
||||
- name: Upload signed Windows CLI release assets
|
||||
if: needs.version.outputs.release != ''
|
||||
shell: pwsh
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
run: |
|
||||
gh release upload "v${{ needs.version.outputs.version }}" `
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64.zip" `
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64.zip" `
|
||||
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline.zip" `
|
||||
--clobber `
|
||||
--repo "${{ needs.version.outputs.repo }}"
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist/cli-*
|
||||
if-no-files-found: error
|
||||
name: opencode-cli-signed-windows
|
||||
path: |
|
||||
packages/opencode/dist/opencode-windows-arm64
|
||||
packages/opencode/dist/opencode-windows-x64
|
||||
packages/opencode/dist/opencode-windows-x64-baseline
|
||||
|
||||
build-electron:
|
||||
# Desktop packaging re-signs the bundled CLI on macOS and Windows, so it starts from the unsigned build.
|
||||
needs:
|
||||
- version
|
||||
- build-cli
|
||||
if: github.repository == 'anomalyco/opencode' && ((github.ref_name == 'dev' && needs.version.outputs.release != '') || github.ref_name == 'beta')
|
||||
- sign-cli-macos
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
|
||||
continue-on-error: false
|
||||
env:
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
@@ -369,7 +403,6 @@ jobs:
|
||||
- host: "windows-2025"
|
||||
target: aarch64-pc-windows-msvc
|
||||
platform_flag: --win --arm64
|
||||
bun_install_flags: --cpu=*
|
||||
- host: "blacksmith-4vcpu-windows-2025"
|
||||
target: x86_64-pc-windows-msvc
|
||||
platform_flag: --win
|
||||
@@ -384,8 +417,9 @@ jobs:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: github.ref_name == 'beta'
|
||||
with:
|
||||
name: opencode-preview-cli-unsigned
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0
|
||||
@@ -446,7 +480,7 @@ jobs:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
|
||||
OPENCODE_CLI_DIST: ${{ github.workspace }}/packages/cli/dist
|
||||
OPENCODE_CLI_DIST: ${{ (github.ref_name == 'beta' && format('{0}/packages/cli/dist', github.workspace)) || '' }}
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
@@ -472,7 +506,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
CSC_KEYCHAIN: build.keychain
|
||||
APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8
|
||||
@@ -486,7 +519,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
|
||||
|
||||
- name: Create macOS .app.tar.gz
|
||||
if: runner.os == 'macOS' && needs.version.outputs.release
|
||||
@@ -571,19 +603,19 @@ jobs:
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: github.ref_name != 'dev' && github.ref_name != 'beta'
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli
|
||||
path: packages/opencode/dist
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: github.ref_name != 'dev' && github.ref_name != 'beta'
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli-windows
|
||||
path: packages/opencode/dist
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: github.ref_name != 'dev' && github.ref_name != 'beta'
|
||||
if: github.ref_name != 'v2' && github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli-signed-windows
|
||||
path: packages/opencode/dist
|
||||
@@ -639,11 +671,23 @@ jobs:
|
||||
git config --global user.name "opencode"
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||
|
||||
- name: Upload desktop release assets
|
||||
if: needs.version.outputs.release
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
files=(/tmp/desktop/*.{exe,blockmap,dmg,zip,AppImage,deb,rpm} /tmp/desktop/*.app.tar.gz)
|
||||
if (( ${#files[@]} == 0 )); then
|
||||
echo "No desktop release assets found"
|
||||
exit 1
|
||||
fi
|
||||
gh release upload "v${{ needs.version.outputs.version }}" "${files[@]}" --clobber --repo "${{ needs.version.outputs.repo }}"
|
||||
|
||||
- run: ./script/publish.ts
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||
OPENCODE_RELEASE_NOTES: ${{ (github.ref_name == 'dev' && inputs.release_notes) || '' }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
GH_REPO: ${{ needs.version.outputs.repo }}
|
||||
@@ -651,22 +695,3 @@ jobs:
|
||||
LATEST_YML_DIR: /tmp/latest-yml
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
OPENCODE_DESKTOP_DIST: /tmp/desktop
|
||||
CLOUDFLARE_ACCOUNT_ID: 15d29c8639fd3733b1b5486a2acfd968
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
notify-discord-v2:
|
||||
needs:
|
||||
- version
|
||||
- publish
|
||||
if: ${{ !cancelled() && github.repository == 'anomalyco/opencode' && github.ref_name == 'dev' && needs.version.outputs.release && needs.publish.result == 'success' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
# Unlike dev, V2 publishes a tag rather than a GitHub Release event.
|
||||
- name: Announce V2 release in Discord
|
||||
uses: SethCohen/github-releases-to-discord@24d166886aee4646d448c8a389ff9e1ebcab3682 # v1.20.0
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
release_name: OpenCode V2 ${{ needs.version.outputs.tag }}
|
||||
release_body: ${{ inputs.release_notes }}
|
||||
release_html_url: https://github.com/${{ github.repository }}/tree/${{ needs.version.outputs.tag }}
|
||||
+16
-34
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- v2
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -48,7 +49,7 @@ jobs:
|
||||
echo "app=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
bun x turbo@2.10.2 ls --affected --filter=@opencode/app --output=json > affected.json
|
||||
bun x turbo@2.10.2 ls --affected --filter=@opencode-ai/app --output=json > affected.json
|
||||
bun -e 'const result = await Bun.file("affected.json").json(); console.log(`app=${result.packages.count > 0}`)' >> "$GITHUB_OUTPUT"
|
||||
|
||||
unit:
|
||||
@@ -93,6 +94,12 @@ jobs:
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
git config --global user.name "opencode"
|
||||
|
||||
- name: Install ffmpeg
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes ffmpeg
|
||||
|
||||
- name: Cache Turbo
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
@@ -105,12 +112,11 @@ jobs:
|
||||
- name: Run unit tests
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
# The runners have four vCPUs, and each Bun test process performs its own concurrent work.
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
GITHUB_ACTIONS=false bun turbo test --concurrency=3
|
||||
GITHUB_ACTIONS=false bun turbo test
|
||||
exit 0
|
||||
fi
|
||||
GITHUB_ACTIONS=false bun turbo test --affected --concurrency=3
|
||||
GITHUB_ACTIONS=false bun turbo test --affected
|
||||
env:
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
||||
TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }}
|
||||
@@ -126,10 +132,10 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
bun turbo verify:package --filter=@opencode/sdk
|
||||
bun turbo verify:package --filter=@opencode-ai/sdk
|
||||
exit 0
|
||||
fi
|
||||
bun turbo verify:package --affected --filter=@opencode/sdk
|
||||
bun turbo verify:package --affected --filter=@opencode-ai/sdk
|
||||
env:
|
||||
TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }}
|
||||
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||
@@ -167,7 +173,7 @@ jobs:
|
||||
|
||||
- name: Check generated documentation
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: services/www
|
||||
working-directory: packages/www
|
||||
run: bun run check:generated
|
||||
|
||||
e2e:
|
||||
@@ -183,7 +189,7 @@ jobs:
|
||||
host: blacksmith-4vcpu-windows-2025
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
env:
|
||||
E2E_ENABLED: ${{ needs.affected.outputs.app == 'true' && github.ref_name != 'dev' && github.head_ref != 'dev' }}
|
||||
E2E_ENABLED: ${{ needs.affected.outputs.app == 'true' && github.ref_name != 'v2' && github.head_ref != 'v2' }}
|
||||
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
|
||||
defaults:
|
||||
run:
|
||||
@@ -231,33 +237,13 @@ jobs:
|
||||
working-directory: packages/app
|
||||
run: bunx playwright install chromium
|
||||
|
||||
- name: Run app e2e tests against production build
|
||||
- name: Run app e2e tests
|
||||
if: env.E2E_ENABLED == 'true'
|
||||
run: bun --cwd packages/app test:e2e:built
|
||||
run: bun --cwd packages/app test:e2e:local
|
||||
env:
|
||||
CI: true
|
||||
timeout-minutes: 30
|
||||
|
||||
- name: Verify service worker precaching and upgrades
|
||||
if: env.E2E_ENABLED == 'true'
|
||||
working-directory: packages/app
|
||||
run: bunx playwright test --config e2e/service-worker/playwright.config.ts
|
||||
timeout-minutes: 5
|
||||
|
||||
- name: Run session UI component tests
|
||||
if: ${{ !cancelled() && env.E2E_ENABLED == 'true' }}
|
||||
run: bun --cwd packages/session-ui test:components
|
||||
env:
|
||||
CI: true
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Run app component tests
|
||||
if: ${{ !cancelled() && env.E2E_ENABLED == 'true' }}
|
||||
run: bun --cwd packages/app test:components
|
||||
env:
|
||||
CI: true
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Upload Playwright artifacts
|
||||
if: always() && env.E2E_ENABLED == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
@@ -268,7 +254,3 @@ jobs:
|
||||
path: |
|
||||
packages/app/e2e/test-results
|
||||
packages/app/e2e/playwright-report
|
||||
packages/session-ui/component-tests/test-results
|
||||
packages/session-ui/component-tests/playwright-report
|
||||
packages/app/component-tests/test-results
|
||||
packages/app/component-tests/playwright-report
|
||||
@@ -0,0 +1,21 @@
|
||||
name: typecheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev, v2]
|
||||
pull_request:
|
||||
branches: [dev, v2]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run typecheck
|
||||
run: bun typecheck
|
||||
@@ -32,8 +32,6 @@ target
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
UPCOMING_CHANGELOG.md
|
||||
RELEASE_REVIEW.md
|
||||
logs/
|
||||
*.bun-build
|
||||
tsconfig.tsbuildinfo
|
||||
/artifacts/
|
||||
+1
-1
@@ -17,4 +17,4 @@ if (process.versions.bun !== expectedBunVersion) {
|
||||
console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`);
|
||||
}
|
||||
'
|
||||
bun run check
|
||||
bun typecheck
|
||||
@@ -2,7 +2,7 @@
|
||||
description: Remove AI code slop
|
||||
---
|
||||
|
||||
Check the diff against `origin/dev`, and remove all AI generated slop introduced in this branch.
|
||||
Check the diff against `origin/v2`, and remove all AI generated slop introduced in this branch.
|
||||
|
||||
This includes:
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode/plugin"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||
...options,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode/plugin"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
|
||||
const TEAM = {
|
||||
tui: ["kommander", "simonklee"],
|
||||
|
||||
+44
-160
@@ -1,167 +1,51 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
|
||||
"jsPlugins": [
|
||||
{ "name": "anti-slop", "specifier": "./script/oxlint/anti-slop/index.ts" },
|
||||
{ "name": "anti-slop-effect", "specifier": "./script/oxlint/anti-slop/effect/index.ts" }
|
||||
],
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"categories": {
|
||||
"correctness": "off",
|
||||
"suspicious": "off",
|
||||
"pedantic": "off",
|
||||
"perf": "off",
|
||||
"style": "off",
|
||||
"restriction": "off",
|
||||
"nursery": "off"
|
||||
"suspicious": "warn"
|
||||
},
|
||||
"rules": {
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
"name": "Reflect",
|
||||
"message": "Use typed property access or direct invocation. Suppress this rule only for genuine reflection."
|
||||
}
|
||||
]
|
||||
"typescript/no-base-to-string": "warn",
|
||||
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
|
||||
"require-yield": "off",
|
||||
// SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime
|
||||
"no-unassigned-vars": "off",
|
||||
// SolidJS tracks reactive deps by reading properties inside createEffect
|
||||
"no-unused-expressions": "off",
|
||||
// Intentional control char matching (ANSI escapes, null byte sanitization)
|
||||
"no-control-regex": "off",
|
||||
// SST and plugin tools require triple-slash references
|
||||
"triple-slash-reference": "off",
|
||||
|
||||
// Suspicious category: suppress noisy rules
|
||||
// Effect's nested function* closures inherently shadow outer scope
|
||||
"no-shadow": "off",
|
||||
// Namespace-heavy codebase makes this too noisy
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
// Opinionated — .sort()/.reverse() mutation is fine in this codebase
|
||||
"unicorn/no-array-sort": "off",
|
||||
"unicorn/no-array-reverse": "off",
|
||||
// Not relevant — this isn't a DOM event handler codebase
|
||||
"unicorn/prefer-add-event-listener": "off",
|
||||
// Bundler handles module resolution
|
||||
"unicorn/require-module-specifiers": "off",
|
||||
// postMessage target origin not relevant for this codebase
|
||||
"unicorn/require-post-message-target-origin": "off",
|
||||
// Side-effectful constructors are intentional in some places
|
||||
"no-new": "off",
|
||||
|
||||
// Type-aware: catch unhandled promises
|
||||
"typescript/no-floating-promises": "warn",
|
||||
// Warn when spreading non-plain objects (Headers, class instances, etc.)
|
||||
"typescript/no-misused-spread": "warn"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"packages/app/**",
|
||||
"packages/desktop/**",
|
||||
"packages/gui-extensions/**",
|
||||
"packages/ui/**",
|
||||
"packages/session-ui/**"
|
||||
],
|
||||
"rules": {
|
||||
"oxc/no-accumulating-spread": "warn",
|
||||
"anti-slop/no-array-filter-map": "warn",
|
||||
"anti-slop/no-reduce-accumulator-copy": "warn",
|
||||
"anti-slop/no-chained-type-assertions": "warn",
|
||||
"anti-slop/no-conditional-empty-object-spread": "warn",
|
||||
"anti-slop/no-known-value-widening": "warn",
|
||||
"anti-slop/no-module-mocking": "warn",
|
||||
"anti-slop/no-object-parameters": "warn",
|
||||
"anti-slop/no-reflect-apply": "warn",
|
||||
"anti-slop/no-reflect-get": "warn",
|
||||
"anti-slop/no-runtime-typeof": "warn",
|
||||
"anti-slop/no-shape-in-symbol-names": "warn",
|
||||
"anti-slop/no-unknown-parameters": "warn",
|
||||
"anti-slop/no-unknown-returns": "warn",
|
||||
"anti-slop/no-unknown-type-aliases": "warn",
|
||||
"anti-slop/no-unsafe-dictionary-type": "warn",
|
||||
"anti-slop/no-widen-then-assert": "warn",
|
||||
"anti-slop/require-readable-spacing": "warn",
|
||||
"anti-slop/require-safety-comment-for-type-assertion": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/app/**", "packages/desktop/**", "packages/gui-extensions/**", "packages/session-ui/**"],
|
||||
"rules": {
|
||||
"anti-slop-effect/no-manual-effect-error-tag": "warn",
|
||||
"anti-slop-effect/no-manual-tag-comparison": "warn",
|
||||
"anti-slop-effect/no-manual-tagged-construction": "warn",
|
||||
"anti-slop-effect/no-service-constructor-imports": "warn",
|
||||
"anti-slop-effect/prefer-effect-match": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/gui-extensions/src/*.ts", "packages/gui-extensions/src/*.tsx"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "solid-js",
|
||||
"importNames": ["createEffect", "createRenderEffect", "createComputed"],
|
||||
"message": "Extension code runs side effects through the SDK: createKeyed(source, fn, { otherwise }) per provider generation or value, createMemo or a plain function for derived values, createLatest for async data, createVisitState for per-visit state, and the handler for logic a user action causes. An escape hatch needs an oxlint-disable comment with a reason."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/gui-extensions/src/*/**"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "solid-js",
|
||||
"importNames": ["createEffect", "createRenderEffect", "createComputed"],
|
||||
"message": "Extension code runs side effects through the SDK: createKeyed(source, fn, { otherwise }) per provider generation or value, createMemo or a plain function for derived values, createLatest for async data, createVisitState for per-visit state, and the handler for logic a user action causes. An escape hatch needs an oxlint-disable comment with a reason."
|
||||
}
|
||||
],
|
||||
"patterns": [
|
||||
{
|
||||
"regex": "^@opencode/(app|desktop)(/|$)",
|
||||
"message": "GUI extensions never import the app or desktop packages. Use the SDK."
|
||||
},
|
||||
{
|
||||
"regex": "^@/",
|
||||
"message": "GUI extensions never import app internals. Use the SDK."
|
||||
},
|
||||
{
|
||||
"group": ["../*/*", "!../*/contract", "!../sdk/*"],
|
||||
"message": "Import another extension only through its contract.ts."
|
||||
},
|
||||
{
|
||||
"regex": "\\.css$",
|
||||
"message": "Import CSS with ?inline and contribute it with ctx.add(Style, css)."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/gui-extensions/src/sdk/**"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"regex": "^@opencode/(app|desktop)(/|$)",
|
||||
"message": "GUI extensions never import the app or desktop packages. Use the SDK."
|
||||
},
|
||||
{
|
||||
"regex": "^@/",
|
||||
"message": "GUI extensions never import app internals. Use the SDK."
|
||||
},
|
||||
{
|
||||
"group": ["../*/*", "!../*/contract", "!../sdk/*"],
|
||||
"message": "Import another extension only through its contract.ts."
|
||||
},
|
||||
{
|
||||
"regex": "\\.css$",
|
||||
"message": "Import CSS with ?inline and contribute it with ctx.add(Style, css)."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"**/node_modules",
|
||||
"**/dist",
|
||||
"**/.build",
|
||||
"**/.sst",
|
||||
"**/*.d.ts",
|
||||
"**/sdk.gen.ts",
|
||||
".agent/**",
|
||||
".agents/**",
|
||||
".claude/**",
|
||||
".codex/**",
|
||||
".continue/**",
|
||||
".cursor/**",
|
||||
".gemini/**",
|
||||
".opencode/**",
|
||||
".pi/**",
|
||||
".roo/**",
|
||||
".windsurf/**",
|
||||
"script/oxlint/anti-slop/**"
|
||||
]
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit generated client files directly.
|
||||
- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk` composes Client, Core, and Server.
|
||||
- This repository does not use Changesets. Do not add `.changeset` files; follow the existing release workflow instead.
|
||||
- The default branch is `dev`. Base branches and worktrees on `origin/dev`, target PRs at `dev`, and diff against `origin/dev` unless told otherwise.
|
||||
- Current implementation changes belong in `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required.
|
||||
- The default branch in this repo is `v2`.
|
||||
- Base all new branches and worktrees on `v2`, or `origin/v2` when the local `v2` ref is unavailable. Do not base them on `dev`.
|
||||
- Local `main` ref may not exist; use `v2` or `origin/v2` for diffs.
|
||||
|
||||
## Live TUI Testing
|
||||
## Live V2 TUI Testing
|
||||
|
||||
- Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode` background server and live sessions.
|
||||
- Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode2` background server and live sessions.
|
||||
- Pass a directory after the script when needed, for example `bun run dev:live /path/to/project`.
|
||||
- The script discovers the server with `opencode service status`, injects its private local credential from `opencode service get password`, and uses the `dev` TUI storage channel so tabs and other client-local state match the installed client.
|
||||
- The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `dev` TUI storage channel so tabs and other client-local state match the installed client.
|
||||
- Prefer `dev:live` over plain `bun run dev` for this workflow. An implicit managed-service connection may replace the live server when the worktree client version differs; explicit `--server` warns and continues without replacing it.
|
||||
|
||||
## TUI Stories
|
||||
## V2 TUI Stories
|
||||
|
||||
- When a user asks for a TUI story, add a fixture-driven story under `packages/tui/src/feature-plugins/system/storybook` and register it in `index.tsx`.
|
||||
- Render the real production component rather than a visual copy. Keep submissions and other side effects local to the story so it is safe to explore repeatedly.
|
||||
@@ -43,7 +45,6 @@ Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributi
|
||||
### General Principles
|
||||
|
||||
- Keep things in one function unless composable or reusable
|
||||
- Validate unknown values once at the boundary that owns them. Pass typed values inward instead of repeating `typeof value === "object"` and property-existence checks. Do not defensively revalidate values already guaranteed by a schema, constructor, or internal type.
|
||||
- Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller.
|
||||
- Before adding complexity for a speculative or vanishingly unlikely race or security edge case, explain the concrete failure mode, likelihood, and complexity cost to the user and get their buy-in. Do not silently expand scope for theoretical robustness.
|
||||
- Avoid `try`/`catch` where possible
|
||||
@@ -51,7 +52,7 @@ Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributi
|
||||
- Use Bun APIs when possible, like `Bun.file()`
|
||||
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
||||
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
||||
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent.js"`) when adding a new config module.
|
||||
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
|
||||
- In Effect generators, bind services to named variables before calling methods. Do not use nested service yields such as `yield* (yield* Foo.Service).bar()`.
|
||||
|
||||
Reduce total variable count by inlining when a value is only used once.
|
||||
@@ -81,9 +82,9 @@ const { a, b } = obj
|
||||
### Imports
|
||||
|
||||
- Never alias imports. Do not use `import { foo as bar } from "..."` or renamed imports like `resolve as pathResolve`.
|
||||
- Never use type-position `import("...")` references such as `Schema.declare<import("@opencode/plugin/effect/plugin").Plugin["effect"]>`. Only when two imports genuinely collide on a name and no other option exists, an aliased type import (`import type { Plugin as PluginDefinition } from "..."`) is permitted as a last resort — still strongly preferred not to.
|
||||
- Never use type-position `import("...")` references such as `Schema.declare<import("@opencode-ai/plugin/effect/plugin").Plugin["effect"]>`. Only when two imports genuinely collide on a name and no other option exists, an aliased type import (`import type { Plugin as PluginDefinition } from "..."`) is permitted as a last resort — still strongly preferred not to.
|
||||
- Never use star imports. Do not use `import * as Foo from "..."` or `import type * as Foo from "..."`.
|
||||
- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode/core/project"`, then reference `Project.ID`.
|
||||
- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode-ai/core/project"`, then reference `Project.ID`.
|
||||
- Prefer dynamic imports for heavy modules that are only needed in selected code paths, especially in startup-sensitive entrypoints. Destructure dynamic import bindings near the top of the narrowest scope that needs them so they read like normal imports. Avoid inline chains such as `await import("./module").then((mod) => mod.value())` or `(await import("./module")).value()`. Keep branch-specific imports inside the branch that needs them to preserve lazy loading.
|
||||
|
||||
### Variables
|
||||
@@ -167,20 +168,21 @@ const table = sqliteTable("session", {
|
||||
- Test actual implementation, do not duplicate logic into tests
|
||||
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package directories such as `packages/core`.
|
||||
|
||||
## Checks
|
||||
## Type Checking
|
||||
|
||||
- Run `bun run check` from the repository root as the canonical full lint and type-check verification.
|
||||
- During focused iteration, run `bun typecheck` from the affected package directory (for example, `packages/core`). Never run `tsc` directly.
|
||||
- Always run `bun typecheck` from package directories (for example, `packages/core`), never `tsc` directly.
|
||||
|
||||
## Session Core
|
||||
## V2 Session Core
|
||||
|
||||
- Keep durable events minimal: record irreducible new facts and do not repeat state derivable by folding the ordered aggregate history. Enrich projections and read models with previous or derived state when consumers need self-contained views.
|
||||
- Keep durable prompt admission separate from model execution. `Session.prompt(...)` publishes `session.inbox.enqueued`, whose projection inserts one durable `session_inbox` row, before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. Delivery publishes `session.inbox.delivered`; its projection consumes the inbox row and inserts the visible message in the same transaction. `session_inbox` stores only unconsumed work.
|
||||
- Reusing a Session ID adopts the existing Session. Reusing a user or synthetic inbox item ID is idempotent when Session and type match: the first admission wins and the retried payload, metadata, and delivery mode are ignored, whether the item is still pending or already delivered (reconciled from the projected message without retained enqueue history). Cross-Session or cross-type reuse fails. Control items keep their operation-specific conflict behavior.
|
||||
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. Interruption targets the active process-local ownership chain for that Session; interruption of a known but idle or locally unowned Session is a no-op, while the public API rejects an unknown Session.
|
||||
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; interruption of a known but idle or locally unowned Session is a no-op, while the public API rejects an unknown Session.
|
||||
- Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.
|
||||
- Each step is one explicit `llm.stream` call that continues from reloaded history. Do not add an in-memory tool loop.
|
||||
- Preserve one explicit `llm.stream(request)` call per Physical Attempt and reload projected history before durable continuation. A logical Step may use generic pre-output retries, one full-context retry after continuation rejection, incomplete-stream continuation, or one overflow-compaction rebuild. Generic retries retain the logical step number and do not consume another agent-step allowance. Do not delegate orchestration to an in-memory tool loop.
|
||||
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. A write-ahead execution claim marks a process-local busy period for restart recovery: terminal completion, failure, or user interruption releases it, while shutdown interruption and process death preserve it. Startup recovery resumes claimed top-level Sessions with durable per-execution attempt accounting. The claim is a recovery marker, not clustered ownership, fencing, or an exactly-once guarantee.
|
||||
- Keep delivery vocabulary explicit. Prompts steer by default. At safe step boundaries, steered compaction takes priority up to the first steered move control; other steers retain enqueue order. At an idle boundary, steers take priority; otherwise exactly one queued item delivers before the runner reevaluates continuation. Inbox items may be cancelled or changed between queue and steer before delivery. Promoting new user input resets the selected agent's step allowance; a batch of steers resets it once.
|
||||
- Keep delivery vocabulary explicit. Prompts steer by default. Steers deliver in enqueue order at safe step boundaries, stopping before compaction or move control items. At an idle boundary, steers take priority; otherwise exactly one queued item delivers before the runner reevaluates continuation. Inbox items may be cancelled or changed between queue and steer before delivery. Promoting new user input resets the selected agent's step allowance; a batch of steers resets it once.
|
||||
- One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle.
|
||||
- Keep event replay ownership separate from clustered Session execution ownership.
|
||||
- Keep the Instructions algebra and built-ins in `src/instructions`; keep instruction producers with their observed domains, and keep Session History selection plus `InstructionState` and `InstructionEntry` persistence Session-owned. `InstructionDiscovery` observes ambient global and upward-project instructions. The runner composes built-ins, discovery, guidance, and entries explicitly in `loadInstructions`; there is no instruction registry.
|
||||
- `session.instructions.updated` stores changed source keys and content hashes and may freeze rendered chronological update text. Blob values live once in `instruction_blob`; the projected `instruction_state` row is the normal boundary-processing source of current and initial values. Request assembly renders the epoch baseline from stored values, while later frozen updates enter history as durable System messages. Completed compaction moves the instruction epoch; Session movement retains it so destination instruction changes are chronological, while committed revert clears it. Forks adopt the parent's newest instruction values even when copied message history ends at an earlier boundary. Unavailable sources retain the last value and block only the initial complete delta.
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
Release notes for OpenCode V2, newest first.
|
||||
|
||||
## v2.0.27 — 2026-10-12
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Built-In Worktree Tools**: Ask OpenCode to use `worktree_create`, `worktree_list`, and `worktree_remove` in Code Mode. You can create isolated worktrees, move sessions into them, and remove them with normal edit approval. ([#53981](https://github.com/anomalyco/opencode/pull/53981))
|
||||
- **Safer Remote Pairing**: The first `opencode pair --remote` now warns that it will expose your server and asks for confirmation. For non-interactive use, pass `opencode pair --remote --yes`. ([#54561](https://github.com/anomalyco/opencode/pull/54561))
|
||||
- **Installable Web App**: Use Chrome or Edge’s **Install** button to install the OpenCode web app, including when it runs behind an authenticating proxy. ([#53905](https://github.com/anomalyco/opencode/pull/53905))
|
||||
- **Clear Session Failures**: Desktop and TUI timelines now show execution errors that happen before or between model steps instead of ending without an explanation. ([#53826](https://github.com/anomalyco/opencode/pull/53826))
|
||||
- **Automatic Project Cleanup**: Projects whose directories no longer exist are automatically hidden from the project list. Opening the directory again restores the project. ([#54354](https://github.com/anomalyco/opencode/pull/54354))
|
||||
|
||||
### Core
|
||||
|
||||
#### Improvements
|
||||
|
||||
- Added the built-in Code Mode tools `opencode.worktree_create`, `opencode.worktree_list`, and `opencode.worktree_remove`. Removing a worktree requires edit approval, and `force: true` can discard modified or untracked files. ([#53981](https://github.com/anomalyco/opencode/pull/53981))
|
||||
- Added confirmation before `opencode pair --remote` first enables internet access, plus `--yes`/`-y` for scripts and other non-interactive use. ([#54561](https://github.com/anomalyco/opencode/pull/54561))
|
||||
- Automatically archived projects when all known directories disappear. Reopening a project restores it, while surviving clones and linked worktrees keep it active. ([#54354](https://github.com/anomalyco/opencode/pull/54354))
|
||||
- Raised the ChatGPT OAuth input limit for `gpt-6.1-sol` and `gpt-6.1-sol-fast` to 500,000 tokens. ([#54668](https://github.com/anomalyco/opencode/pull/54668))
|
||||
- Added Vertex AI routes for Interactions and Mistral models. ([#54175](https://github.com/anomalyco/opencode/pull/54175), [#54058](https://github.com/anomalyco/opencode/pull/54058))
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Fixed updater detection for symlinked home directories and package-manager installations, increased probe timeouts, and now report unavailable updates clearly when the installation method cannot be detected. ([#54609](https://github.com/anomalyco/opencode/pull/54609), [#54622](https://github.com/anomalyco/opencode/pull/54622), [#54672](https://github.com/anomalyco/opencode/pull/54672))
|
||||
- Fixed GitHub Copilot routing for Gemini, Anthropic, and Responses models when live model metadata is unavailable, and accepted interleaved Claude reasoning values. ([#54208](https://github.com/anomalyco/opencode/pull/54208), [#54210](https://github.com/anomalyco/opencode/pull/54210), [#54201](https://github.com/anomalyco/opencode/pull/54201))
|
||||
- Fixed Google Vertex Application Default Credentials when `CLOUDSDK_CONFIG` points to a custom gcloud configuration directory. Thanks [@H-Max](https://github.com/H-Max). ([#54164](https://github.com/anomalyco/opencode/pull/54164))
|
||||
- Prevented ChatGPT token-sharing sessions from sending unsupported mid-session reasoning-effort updates. ([#54085](https://github.com/anomalyco/opencode/pull/54085))
|
||||
- Blocked `file://` access through Code Mode’s `fetch`, which now returns a clear filesystem-access error. Thanks [@ferdinandyb](https://github.com/ferdinandyb). ([#54510](https://github.com/anomalyco/opencode/pull/54510))
|
||||
- Fixed remote workspace file operations so driver-provided filesystem overrides are used correctly.
|
||||
- Preserved subagent notifications when location services are evicted, and propagated well-known configuration updates across locations with bounded HTTP timeouts. ([#54072](https://github.com/anomalyco/opencode/pull/54072), [#53821](https://github.com/anomalyco/opencode/pull/53821))
|
||||
|
||||
### TUI
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Displayed session execution failures that occur before or between model steps directly in the timeline. ([#53826](https://github.com/anomalyco/opencode/pull/53826))
|
||||
- Preserved a saved session’s unavailable model selection instead of silently replacing it with a fallback. ([#53822](https://github.com/anomalyco/opencode/pull/53822))
|
||||
- Added syntax highlighting for C++ module interface files such as `.cppm`, `.ixx`, and `.cxxm`. ([#53853](https://github.com/anomalyco/opencode/pull/53853))
|
||||
- Summarized integrations with multiple credentials as a connection count, preventing long labels from crowding the integration name. ([#54054](https://github.com/anomalyco/opencode/pull/54054))
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Improvements
|
||||
|
||||
- Made the web app installable from Chrome and Edge, including deployments behind cookie-based authenticating proxies. ([#53905](https://github.com/anomalyco/opencode/pull/53905))
|
||||
- Updated the session column layout and text wrapping for clearer long content. ([#54020](https://github.com/anomalyco/opencode/pull/54020))
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Displayed session execution failures that occur before or between model steps directly in the timeline. ([#53826](https://github.com/anomalyco/opencode/pull/53826))
|
||||
- Fixed command-palette selection so newly arriving higher-ranked results become active until you explicitly choose a result. ([#54275](https://github.com/anomalyco/opencode/pull/54275))
|
||||
- Preserved bold, code, emphasis, and strike-through formatting inside Markdown link labels. ([#54451](https://github.com/anomalyco/opencode/pull/54451))
|
||||
- Fixed timeline file links and complete patch rendering, including unusual filenames and patches whose original alignment can be reused. ([#53641](https://github.com/anomalyco/opencode/pull/53641), [#54017](https://github.com/anomalyco/opencode/pull/54017))
|
||||
- Showed complete tool error text when an error is expanded. ([#53816](https://github.com/anomalyco/opencode/pull/53816))
|
||||
- Fixed browser-panel controls, page visibility, corner masks, and closed side-region overlays. ([#53907](https://github.com/anomalyco/opencode/pull/53907), [#54038](https://github.com/anomalyco/opencode/pull/54038), [#52461](https://github.com/anomalyco/opencode/pull/52461), [#54052](https://github.com/anomalyco/opencode/pull/54052))
|
||||
- Made the third-party notices dialog scrollable. ([#53911](https://github.com/anomalyco/opencode/pull/53911))
|
||||
|
||||
### SDK
|
||||
|
||||
#### Improvements
|
||||
|
||||
- Added startup plugin seeding to Effect embedded hosts and workerd through `OpenCode.create({ plugins })` and `OpenCodeWorkerd.create({ plugins })`.
|
||||
- Re-exported `WorkspaceDriver`, `EnvironmentDriver`, and `EnvironmentFiles` from `@opencode/sdk/effect`.
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Ensured embedded hosts activate supplied plugins before model discovery, generation, and recovery begin. ([#54219](https://github.com/anomalyco/opencode/pull/54219))
|
||||
|
||||
### Extensions
|
||||
|
||||
#### Improvements
|
||||
|
||||
- Extended `BridgeLayout` with viewport dimensions and optional border information so native extension surfaces can track window resizing and preserve rounded card borders.
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Kept desktop browser-panel corner masks and borders aligned while resizing or using custom themes. ([#52461](https://github.com/anomalyco/opencode/pull/52461))
|
||||
- Kept the browser site button clickable after the embedded page takes focus. ([#53907](https://github.com/anomalyco/opencode/pull/53907))
|
||||
|
||||
## v2.0.26 — 2026-10-08
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Google Cloud Credentials for Vertex**: Run `/connect`, choose **Vertex**, then select **Google Cloud credentials (gcloud auth or environment)** to use Application Default Credentials. You can choose or enter the Google Cloud project and location. ([#53798](https://github.com/anomalyco/opencode/pull/53798))
|
||||
- **Faster, Safer Session Snapshots**: Session snapshots now add less overhead, recover from interrupted or concurrent captures, restore large selections faster, and pack stored objects to reduce disk usage. ([#51996](https://github.com/anomalyco/opencode/pull/51996))
|
||||
- **Working Transcript Links**: You can click links anywhere in a transcript, including any row of a wrapped URL. Hyperlinks also work in more terminals, and `FORCE_HYPERLINK=1` can enable them when automatic detection does not. ([#53903](https://github.com/anomalyco/opencode/pull/53903), [#53904](https://github.com/anomalyco/opencode/pull/53904))
|
||||
- **Step 5 Preview Free**: You can now select **Step 5 Preview Free** from OpenCode’s free model lineup for a limited time.
|
||||
|
||||
### Core
|
||||
|
||||
#### Improvements
|
||||
|
||||
- Added **Google Cloud credentials (gcloud auth or environment)** to the Vertex connection flow, with project and location choices discovered from environment variables, gcloud configurations, and Application Default Credentials. ([#53798](https://github.com/anomalyco/opencode/pull/53798))
|
||||
- Reduced session snapshot overhead, accelerated multi-file restores, made capture resilient to crashes and concurrent processes, and added lossless background packing of snapshot objects. ([#51996](https://github.com/anomalyco/opencode/pull/51996))
|
||||
- Added **Step 5 Preview Free** to OpenCode’s free models for a limited time and removed the retired **Fledge Alpha Free** listing.
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Prevented working-tree diffs from timing out or stalling the server in repositories with many untracked files by batching Git operations. ([#53449](https://github.com/anomalyco/opencode/pull/53449))
|
||||
- Fixed large revert selections failing when their paths exceeded the operating system’s command-line limit. ([#53956](https://github.com/anomalyco/opencode/pull/53956))
|
||||
- Fixed snapshot and undo handling for literal filenames containing Git pathspec characters, corrupted or locked snapshot indexes, concurrent captures, and project directories whose names begin with `..`. ([#51996](https://github.com/anomalyco/opencode/pull/51996))
|
||||
- Changed `opencode service` remote-access URLs to use a persistent random subdomain rather than a predictable route. Disabling and re-enabling remote access generates a new address. ([#53962](https://github.com/anomalyco/opencode/pull/53962))
|
||||
|
||||
### TUI
|
||||
|
||||
#### Improvements
|
||||
|
||||
- Enabled OSC 8 hyperlinks in more terminals, including VS Code and other xterm.js terminals, Zed, mintty, and newer VTE terminals; `FORCE_HYPERLINK=1` now overrides terminal detection. ([#53904](https://github.com/anomalyco/opencode/pull/53904))
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Fixed transcript links not opening when clicked, including URLs wrapped across multiple rows. ([#53903](https://github.com/anomalyco/opencode/pull/53903))
|
||||
- Kept activity summaries in place when expanding or collapsing details with `session.verbosity: "low"`. ([#52357](https://github.com/anomalyco/opencode/pull/52357))
|
||||
- Prevented the home-screen version footer from overlapping keyboard hints in short terminals. Thanks [@fihaaade](https://github.com/fihaaade). ([#53891](https://github.com/anomalyco/opencode/pull/53891))
|
||||
+1
-1
@@ -81,7 +81,7 @@ Bug fixes, chores, and tests must reference an existing issue. Documentation, re
|
||||
|
||||
Before implementing new functionality, open a feature request describing the problem, why it belongs in OpenCode, and your proposed approach if you have one. Wait for design approval before opening the implementation PR.
|
||||
|
||||
Base branches on `dev` and complete the provided pull request template.
|
||||
Base branches on `v2`, not `dev`, and complete the provided pull request template.
|
||||
|
||||
### Keep It Focused
|
||||
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
# V2 HTTP API audit checklist
|
||||
|
||||
**Source:** `packages/protocol/openapi.json`
|
||||
**Current endpoint count:** 139
|
||||
**Last regenerated:** 2026-09-13
|
||||
|
||||
## How to use this checklist
|
||||
|
||||
Review endpoints in document order. For each endpoint, select one disposition and capture rationale or follow-up work in Notes. Mark **Reviewed** only after the disposition is agreed.
|
||||
|
||||
### Review criteria
|
||||
|
||||
- Resource and operation naming
|
||||
- HTTP method and idempotency
|
||||
- Request parameters and location scope
|
||||
- Response shape and error taxonomy
|
||||
- Authentication and authorization
|
||||
- Current production consumers
|
||||
- Stability level: public, experimental, or internal
|
||||
- Whether the generated client API is intuitive
|
||||
|
||||
### Disposition legend
|
||||
|
||||
- **Keep:** ship unchanged as a supported V2 API
|
||||
- **Change:** retain after a defined contract change
|
||||
- **Remove:** exclude from the official V2 API
|
||||
- **Experimental-only:** retain outside the stable API commitment
|
||||
|
||||
## Progress
|
||||
|
||||
- [x] Group 1: Foundation and placement (4)
|
||||
- [x] Group 2: Configuration and capability catalogs (16)
|
||||
- [x] Group 3: Credentials, integrations, MCP, and web search (22)
|
||||
- [x] Group 4: Session lifecycle (12)
|
||||
- [x] Group 5: Session execution and inputs (11)
|
||||
- [x] Group 6: Session history and recovery (13)
|
||||
- [x] Group 7: Inbox, permissions, and forms (19)
|
||||
- [x] Group 8: Filesystem, worktrees, and VCS (12)
|
||||
- [x] Group 9: PTYs, persistent terminals, and shells (24)
|
||||
- [x] Group 10: Events, RPC, and experimental operations (6)
|
||||
|
||||
## Resolved during audit
|
||||
|
||||
### [x] `POST /api/plugin/await-activation`
|
||||
|
||||
- **Decision:** Remove
|
||||
- **Notes:** Activation timing is an internal server concern. Catalog reads remain non-blocking.
|
||||
|
||||
### [x] Location response wrappers
|
||||
|
||||
- **Decision:** Reduce generic endpoint response locations to `{ directory }`.
|
||||
- **Notes:** Full project metadata remains available from `GET /api/location`; no consumers used it from wrapped responses.
|
||||
|
||||
### [x] `GET /api/health` and `GET /api/server`
|
||||
|
||||
- **Decision:** Merge and rename
|
||||
- **Replacement:** `GET /api/info` with operation ID `server.info`.
|
||||
- **Notes:** Returns `version`, `pid`, and connection `urls`; readiness is conveyed by HTTP status.
|
||||
|
||||
### [x] `GET /api/project/current`
|
||||
|
||||
- **Decision:** Remove
|
||||
- **Replacement:** `GET /api/location`, using `project` from the response.
|
||||
- **Notes:** The endpoint duplicated `Location.Info.project`; production callers were migrated.
|
||||
|
||||
### [x] `POST /api/workspace` and `DELETE /api/workspace/{workspaceID}`
|
||||
|
||||
- **Decision:** Remove
|
||||
- **Notes:** Provider-backed workspaces are not part of the V2 HTTP contract and can be introduced later. Core and the embedded SDK retain internal workspace support.
|
||||
|
||||
## Group 1: Foundation and placement
|
||||
|
||||
**Endpoints:** 4
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 001–002 | `GET` | `/api/info` | `server.info` | Keep | Replaces the former health and server endpoints. |
|
||||
| [x] 003 | `GET` | `/api/location` | `location.get` | Keep | Workspace selectors and response fields removed until workspace support ships. |
|
||||
| [x] 004 | `GET` | `/api/project` | `project.list` | Keep | Removed unused `time.initialized`; the database column remains for migration data. |
|
||||
| [x] 005 | `PATCH` | `/api/project/{projectID}` | `project.update` | Keep | Request and response accepted as-is. |
|
||||
|
||||
## Group 2: Configuration and capability catalogs
|
||||
|
||||
**Endpoints:** 16
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 008 | `GET` | `/api/agent` | `agent.list` | Keep | Request and response accepted as-is. |
|
||||
| [x] 009 | `GET` | `/api/agent/{agentID}` | `agent.get` | Keep | Request, response, and not-found error accepted as-is. |
|
||||
| [x] 010 | `GET` | `/api/plugin` | `plugin.list` | Keep | Request and response accepted as-is. |
|
||||
| [x] 012 | `POST` | `/api/plugin/check` | `plugin.check` | Keep | Request and response accepted as-is. |
|
||||
| [x] 013 | `POST` | `/api/plugin/update` | `plugin.update` | Keep | Request and errors accepted as-is. |
|
||||
| [x] 014 | `GET` | `/api/model` | `model.list` | Keep | Request and response accepted as-is. |
|
||||
| [x] 015 | `GET` | `/api/model/default` | `model.default` | Keep | Request and nullable response accepted as-is. |
|
||||
| [x] 016 | `GET` | `/api/provider` | `provider.list` | Keep | Request and response accepted as-is. |
|
||||
| [x] 017 | `GET` | `/api/provider/{providerID}` | `provider.get` | Keep | Request, response, and not-found error accepted as-is. |
|
||||
| [x] 018 | `GET` | `/api/command` | `command.list` | Keep | Request and response accepted as-is. |
|
||||
| [x] 019 | `GET` | `/api/skill` | `skill.list` | Keep | Renamed `location` to `path`; removed the skill-specific `slash` flag and slash-command behavior. |
|
||||
| [x] 020 | `GET` | `/api/reference` | `reference.list` | Keep | Removed duplicate `description` and `hidden` fields from nested `source`. |
|
||||
| [x] 021 | `GET` | `/api/config` | `config.get` | Keep | Compatibility entries removed; response now contains only documents and OpenCode directories. |
|
||||
| [x] 022 | `GET` | `/api/config/preferences` | `config.preferences` | Remove | Redundant special projection of global config. |
|
||||
| [x] 023 | `PATCH` | `/api/config/preferences` | `config.updatePreferences` | Remove | Redundant field-specific config mutation API. |
|
||||
| [x] 024 | `GET` | `/api/config/shell` | `config.shells` | Keep | Required by the server Terminal shell setting. |
|
||||
| [x] 024a | `PATCH` | `/api/experimental/config` | `experimental.config.update` | Change | Experimental global config mutation; initially accepts only `shell`. |
|
||||
|
||||
## Group 3: Credentials, integrations, MCP, and web search
|
||||
|
||||
**Endpoints:** 22
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 025 | `GET` | `/api/integration` | `integration.list` | Keep | Full integration inventory is consumed by authentication and integration-selection clients. |
|
||||
| [x] 026 | `GET` | `/api/integration/{integrationID}` | `integration.get` | Change | Missing integration now returns typed `404` instead of optional data. |
|
||||
| [x] 027 | `POST` | `/api/experimental/integration/wellknown` | `experimental.integration.wellknown.add` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 028 | `POST` | `/api/integration/{integrationID}/connect/key` | `integration.connect.key` | Change | Missing integration returns typed `404`; key form answers retained. |
|
||||
| [x] 029 | `POST` | `/api/integration/{integrationID}/connect/oauth` | `integration.oauth.connect` | Keep | OAuth connection start contract retained. |
|
||||
| [x] 030 | `GET` | `/api/integration/{integrationID}/connect/oauth/{attemptID}` | `integration.oauth.status` | Change | Missing integration or OAuth attempt returns typed `404`. |
|
||||
| [x] 031 | `DELETE` | `/api/integration/{integrationID}/connect/oauth/{attemptID}` | `integration.oauth.cancel` | Keep | Idempotent cancellation remains a no-op for unavailable or terminal attempts. |
|
||||
| [x] 032 | `POST` | `/api/integration/{integrationID}/connect/oauth/{attemptID}/complete` | `integration.oauth.complete` | Change | Missing integration or OAuth attempt returns typed `404`; code remains mode-dependent. |
|
||||
| [x] 033 | `POST` | `/api/integration/{integrationID}/connect/command` | `integration.command.connect` | Change | Missing integration or command method returns typed `404`. |
|
||||
| [x] 034 | `GET` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.status` | Change | Missing integration or command attempt returns typed `404`. |
|
||||
| [x] 035 | `DELETE` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.cancel` | Keep | Idempotent cancellation remains a no-op for unavailable or terminal attempts. |
|
||||
| [x] 036 | `GET` | `/api/mcp` | `mcp.list` | Keep | MCP inventory and connection status retained. |
|
||||
| [x] 037 | `PUT` | `/api/experimental/mcp/{server}` | `experimental.mcp.add` | Experimental-only | Runtime-only MCP override; does not persist configuration. |
|
||||
| [x] 038 | `DELETE` | `/api/experimental/mcp/{server}` | `experimental.mcp.remove` | Experimental-only | Runtime removal override; missing server returns `404`. |
|
||||
| [x] 039 | `POST` | `/api/experimental/mcp/{server}/connect` | `experimental.mcp.connect` | Experimental-only | Runtime connection override retained outside the stable API. |
|
||||
| [x] 040 | `POST` | `/api/experimental/mcp/{server}/disconnect` | `experimental.mcp.disconnect` | Experimental-only | Runtime disconnection override retained outside the stable API. |
|
||||
| [x] 041 | `GET` | `/api/mcp/resource` | `mcp.resource.catalog` | Keep | Reviewed separately by coworker. |
|
||||
| [x] 042 | `PATCH` | `/api/credential/{credentialID}` | `credential.update` | Change | Removed redundant location query; credentials and events are global. |
|
||||
| [x] 043 | `DELETE` | `/api/credential/{credentialID}` | `credential.remove` | Change | Removed redundant location query; credentials and events are global. |
|
||||
| [x] 044 | `POST` | `/api/credential/{credentialID}/activate` | `credential.activate` | Change | Removed redundant location query; credentials and events are global. |
|
||||
| [x] 045 | `GET` | `/api/websearch/provider` | `websearch.providers` | Keep | Provider availability remains location-scoped; singular resource path retained. |
|
||||
| [x] 046 | `POST` | `/api/websearch` | `websearch.query` | Keep | Unknown provider remains an invalid request; published time documented as Unix epoch milliseconds. |
|
||||
|
||||
## Group 4: Session lifecycle
|
||||
|
||||
**Endpoints:** 12
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 047 | `GET` | `/api/session` | `session.list` | Keep | Existing filtering, ordering, and cursor contract retained for now. |
|
||||
| [x] 048 | `POST` | `/api/session` | `session.create` | Keep | Existing creation contract retained; model reference includes optional variant. |
|
||||
| [x] 049 | `GET` | `/api/experimental/session/stats` | `experimental.session.stats` | Experimental-only | Session analytics retained outside the stable API commitment. |
|
||||
| [x] 050 | `GET` | `/api/session/active` | `session.active` | Keep | Status record retained for future active-state expansion. |
|
||||
| [x] 051 | `GET` | `/api/session/{sessionID}` | `session.get` | Keep | Specific session read and typed `404` retained. |
|
||||
| [x] 052 | `DELETE` | `/api/session/{sessionID}` | `session.remove` | Keep | Session and child deletion with typed `404` retained. |
|
||||
| [x] 053 | `POST` | `/api/session/{sessionID}/fork` | `session.fork` | Change | Request now accepts optional branded `before` message ID; omission copies full history. |
|
||||
| [x] 054 | `POST` | `/api/session/{sessionID}/agent` | `session.switchAgent` | Keep | Subsequent-execution agent selection retained. |
|
||||
| [x] 055 | `POST` | `/api/session/{sessionID}/model` | `session.switchModel` | Keep | Subsequent-execution model and optional variant selection retained. |
|
||||
| [x] 056 | `PATCH` | `/api/session/{sessionID}` | `session.update` | Change | General session patch updates title and permissions; rules emit `session.permissions`. |
|
||||
| [x] 057 | `POST` | `/api/session/{sessionID}/move` | `session.move` | Change | Removed inaccurate local-change transfer claim; delivery behavior retained. |
|
||||
| [x] 058 | `POST` | `/api/session/{sessionID}/background` | `session.background` | Keep | Backgroundable foreground tools transition to background observation; idle requests remain no-ops. |
|
||||
|
||||
## Group 5: Session execution and inputs
|
||||
|
||||
**Endpoints:** 11
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 059 | `POST` | `/api/session/{sessionID}/prompt` | `session.prompt` | Keep | Durable admission, delivery mode, and admit-only resume control retained. |
|
||||
| [x] 060 | `POST` | `/api/session/{sessionID}/command` | `session.command` | Change | Renamed request field from `command` to `name`; `204` retained. |
|
||||
| [x] 061 | `POST` | `/api/experimental/session/{sessionID}/skill` | `experimental.session.skill` | Experimental-only | Skill ID is now the `id` field; standalone activation remains experimental. |
|
||||
| [x] 062 | `POST` | `/api/session/{sessionID}/synthetic` | `session.synthetic` | Keep | Durable synthetic admission and delivery controls retained. |
|
||||
| [x] 063 | `POST` | `/api/session/{sessionID}/shell` | `session.shell` | Change | Caller ID is now the optimistic shell message ID; server derives its event ID. |
|
||||
| [x] 064 | `POST` | `/api/session/{sessionID}/compact` | `session.compact` | Keep | Durable compaction admission and delivery controls retained. |
|
||||
| [x] 065 | `POST` | `/api/experimental/session/{sessionID}/wait` | `experimental.session.wait` | Experimental-only | Race-free idle barrier retained outside the stable API. |
|
||||
| [x] 066 | `POST` | `/api/session/{sessionID}/generate` | `session.generate` | Keep | Transient generation from session context retained. |
|
||||
| [x] 067 | `POST` | `/api/session/{sessionID}/interrupt` | `session.interrupt` | Change | Renamed `continue` to `resume` across public and internal interruption APIs. |
|
||||
| [x] 068 | `PUT` | `/api/session/{sessionID}/environment` | `session.environment` | Keep | Process-local environment replacement retained in the stable API. |
|
||||
| [x] 069 | `POST` | `/api/session/{sessionID}/view` | `session.view` | Change | Idle watermark now uses the standard epoch-millisecond timestamp schema. |
|
||||
|
||||
## Group 6: Session history and recovery
|
||||
|
||||
**Endpoints:** 13
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 070 | `POST` | `/api/experimental/session/import` | `experimental.session.import` | Experimental-only | Existing projected transcript import contract retained outside the stable API. |
|
||||
| [x] 071 | `GET` | `/api/experimental/session/{sessionID}/export` | `experimental.session.export` | Experimental-only | Existing projected transcript export contract retained outside the stable API. |
|
||||
| [x] 072 | `POST` | `/api/session/{sessionID}/revert/stage` | `session.revert.stage` | Keep | Existing staged history and optional file restoration behavior retained. |
|
||||
| [x] 073 | `DELETE` | `/api/session/{sessionID}/revert` | `session.revert.clear` | Change | Clearing staged revert now deletes the session revert resource. |
|
||||
| [x] 074 | `POST` | `/api/session/{sessionID}/revert/commit` | `session.revert.commit` | Keep | Explicit staged-revert commit action retained. |
|
||||
| [x] 075 | `GET` | `/api/session/{sessionID}/context` | `session.context` | Keep | Active model-context projection retained. |
|
||||
| [x] 076 | `GET` | `/api/session/{sessionID}/diff` | `session.diff` | Keep | Turn-range structured diff contract retained. |
|
||||
| [x] 077 | `GET` | `/api/experimental/session/{sessionID}/instructions/entries` | `experimental.session.instructions.entry.list` | Experimental-only | API-managed durable context entries retained outside the stable API. |
|
||||
| [x] 078 | `PUT` | `/api/experimental/session/{sessionID}/instructions/entries/{key}` | `experimental.session.instructions.entry.put` | Experimental-only | API-managed durable context entries retained outside the stable API. |
|
||||
| [x] 079 | `DELETE` | `/api/experimental/session/{sessionID}/instructions/entries/{key}` | `experimental.session.instructions.entry.remove` | Experimental-only | API-managed durable context entries retained outside the stable API. |
|
||||
| [x] 080 | `GET` | `/api/experimental/session/{sessionID}/log` | `session.log` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 081 | `GET` | `/api/session/{sessionID}/message/{messageID}` | `session.message.get` | Change | Normalized specific-message operation ID. |
|
||||
| [x] 082 | `GET` | `/api/session/{sessionID}/message` | `session.message.list` | Change | Normalized session-scoped message-list operation ID. |
|
||||
|
||||
## Group 7: Inbox, permissions, and forms
|
||||
|
||||
**Endpoints:** 19
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 083 | `GET` | `/api/session/{sessionID}/inbox` | `session.inbox.list` | Change | Inbox timestamps now use the standard nested `time.created` shape. |
|
||||
| [x] 084 | `DELETE` | `/api/session/{sessionID}/inbox/{inboxID}` | `session.inbox.cancel` | Change | Cancellation is idempotent and returns `204` when the session exists. |
|
||||
| [x] 085 | `PATCH` | `/api/session/{sessionID}/inbox/{inboxID}` | `session.inbox.update` | Change | Consolidated delivery mutation with `delivery: "steer" | "queue"`. |
|
||||
| [x] 086 | — | — | — | Remove | Replaced by `session.inbox.update`. |
|
||||
| [x] 087 | `GET` | `/api/form` | `form.list` | Change | Removed redundant `request` path and operation namespace. |
|
||||
| [x] 088 | `GET` | `/api/session/{sessionID}/form` | `session.form.list` | Keep | Pending session form list retained with temporary MCP sentinel compatibility. |
|
||||
| [x] 089 | `POST` | `/api/session/{sessionID}/form` | `session.form.create` | Keep | External form creation and temporary MCP sentinel ownership retained. |
|
||||
| [x] 090 | `GET` | `/api/session/{sessionID}/form/{formID}` | `session.form.get` | Change | Form definition and lifecycle state are now returned together. |
|
||||
| [x] 091 | — | — | — | Remove | State is included by `session.form.get`. |
|
||||
| [x] 092 | `POST` | `/api/session/{sessionID}/form/{formID}/reply` | `session.form.reply` | Keep | One-shot validated form reply retained. |
|
||||
| [x] 093 | `DELETE` | `/api/session/{sessionID}/form/{formID}` | `session.form.cancel` | Change | Form cancellation now deletes the pending form resource. |
|
||||
| [x] 094 | `GET` | `/api/permission/request` | `permission.request.list` | Keep | Pending-request namespace retained alongside saved permissions. |
|
||||
| [x] 095 | `GET` | `/api/permission/saved` | `permission.saved.list` | Change | Added persisted creation and update timestamps under `time`. |
|
||||
| [x] 096 | `DELETE` | `/api/permission/saved/{id}` | `permission.saved.remove` | Keep | Idempotent saved-permission deletion retained. |
|
||||
| [x] 097 | `POST` | `/api/session/{sessionID}/permission` | `session.permission.create` | Keep | Non-blocking permission evaluation and pending-request creation retained. |
|
||||
| [x] 098 | `GET` | `/api/session/{sessionID}/permission` | `session.permission.list` | Keep | Pending session permission list retained. |
|
||||
| [x] 099 | `GET` | `/api/session/{sessionID}/permission/{requestID}` | `session.permission.get` | Keep | Specific pending permission read with ownership validation retained. |
|
||||
| [x] 100 | `POST` | `/api/session/{sessionID}/permission/{requestID}/reply` | `session.permission.reply` | Change | Renamed request field from `reply` to `decision`. |
|
||||
| [x] 101 | — | — | — | Remove | Permission rules are updated through `session.update`. |
|
||||
|
||||
## Group 8: Filesystem, worktrees, and VCS
|
||||
|
||||
**Endpoints:** 12
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 102 | `GET` | `/api/fs/read/*` | `fs.read` | Keep | Relative wildcard file reads and raw byte responses retained. |
|
||||
| [x] 103 | `GET` | `/api/fs/list` | `fs.list` | Keep | Existing path scope and minimal entry metadata retained. |
|
||||
| [x] 104 | `GET` | `/api/fs/find` | `fs.find` | Keep | Existing ranked filesystem search retained. |
|
||||
| [x] 105 | `GET` | `/api/worktree` | `worktree.list` | Keep | Reviewed separately by coworker. |
|
||||
| [x] 106 | `POST` | `/api/worktree` | `worktree.create` | Keep | Reviewed separately by coworker. |
|
||||
| [x] 107 | `DELETE` | `/api/worktree` | `worktree.remove` | Keep | Reviewed separately by coworker. |
|
||||
| [x] 108 | `POST` | `/api/worktree/refresh` | `worktree.refresh` | Keep | Reviewed separately by coworker. |
|
||||
| [x] 109 | `GET` | `/api/vcs` | `vcs.get` | Change | Preserved branch nesting and added selected VCS provider ID. |
|
||||
| [x] 110 | `GET` | `/api/vcs/base` | `vcs.base` | Keep | Review-base inference and nullable unavailable state retained. |
|
||||
| [x] 111 | `GET` | `/api/vcs/status` | `vcs.status` | Keep | Existing working-copy status shape retained for now. |
|
||||
| [x] 112 | `GET` | `/api/vcs/branch` | `vcs.branch.list` | Change | Singular collection path and normalized operation ID. |
|
||||
| [x] 113 | `GET` | `/api/vcs/diff` | `vcs.diff` | Keep | Existing working, branch, and committed comparison modes retained. |
|
||||
|
||||
## Group 9: PTYs, persistent terminals, and shells
|
||||
|
||||
**Endpoints:** 24
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 114 | `GET` | `/api/pty` | `pty.list` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 115 | `POST` | `/api/pty` | `pty.create` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 116 | `GET` | `/api/pty/{ptyID}` | `pty.get` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 117 | `PUT` | `/api/pty/{ptyID}` | `pty.update` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 118 | `DELETE` | `/api/pty/{ptyID}` | `pty.remove` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 119 | `POST` | `/api/pty/{ptyID}/connect-token` | `pty.connect.token` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 120 | `GET` | `/api/pty/{ptyID}/connect` | `pty.connect` | Keep | PTY endpoints reviewed together and retained. |
|
||||
| [x] 121 | `GET` | `/api/experimental/session/{sessionID}/terminal/read` | `server.experimental.persistentPty.read` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 122 | `GET` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.list` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 123 | `POST` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.create` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 124 | `POST` | `/api/experimental/persistent-pty/shutdown` | `server.experimental.persistentPty.shutdown` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 125 | `POST` | `/api/experimental/persistent-pty/handoff` | `server.experimental.persistentPty.handoff` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 126 | `GET` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.get` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 127 | `PUT` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.update` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 128 | `DELETE` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.remove` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 129 | `GET` | `/api/experimental/persistent-pty/{ptyID}/snapshot` | `server.experimental.persistentPty.snapshot` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 130 | `POST` | `/api/experimental/persistent-pty/{ptyID}/connect-token` | `server.experimental.persistentPty.connectToken` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 131 | `GET` | `/api/experimental/persistent-pty/{ptyID}/connect` | `persistentPty.connect` | Experimental-only | Retained outside the stable API commitment. |
|
||||
| [x] 132 | `GET` | `/api/shell` | `shell.list` | Change | Stable shell inventory retained; numeric timestamps documented as epoch milliseconds. |
|
||||
| [x] 133 | `POST` | `/api/shell` | `shell.create` | Change | Timeout is optional and defaults to zero; caller metadata retained. |
|
||||
| [x] 134 | `GET` | `/api/shell/{id}` | `shell.get` | Keep | Specific running or retained shell read retained. |
|
||||
| [x] 135 | `DELETE` | `/api/shell/{id}` | `shell.remove` | Change | Shell deletion is idempotent and returns `204` when already absent. |
|
||||
| [x] 136 | — | — | — | Remove | Timeout mutation remains an internal Core shell operation. |
|
||||
| [x] 137 | `GET` | `/api/shell/{id}/output` | `shell.output` | Keep | Existing byte-cursor text output paging retained. |
|
||||
|
||||
## Group 10: Events, RPC, and experimental operations
|
||||
|
||||
**Endpoints:** 6
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [x] 138 | `POST` | `/api/experimental/generate` | `experimental.generate.text` | Experimental-only | Stateless generation retained alongside session generation. |
|
||||
| [x] 139 | `POST` | `/api/rpc/{rpcID}/{method}` | `rpc.call` | Keep | Generic typed-error plugin RPC transport retained. |
|
||||
| [x] 140 | `GET` | `/api/event` | `event.subscribe` | Keep | Unified native and dynamic plugin event stream retained. |
|
||||
| [x] 141 | `GET` | `/api/debug/location` | `debug.location.list` | Keep | Loaded-location debug inventory retained. |
|
||||
| [x] 142 | `DELETE` | `/api/debug/location` | `debug.location.evict` | Keep | Idempotent loaded-location eviction retained. |
|
||||
| [x] 143 | `GET` | `/api/experimental/migration/v1` | `experimental.migration.v1.status` | Experimental-only | Retained outside the stable API commitment. |
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.remotion
|
||||
out/frame-*.png
|
||||
@@ -0,0 +1,483 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "glm52-rise-video",
|
||||
"dependencies": {
|
||||
"@remotion/cli": "^4.0.384",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"remotion": "^4.0.384",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "^5.8.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.24.1", "", { "bin": "./bin/babel-parser.js" }, "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.24.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@mediabunny/aac-encoder": ["@mediabunny/aac-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-JNzgdJoHMFFnv5imi1+dmjZMudsJ1zNCUCEkKjBh90cqGhAFg8xu4V2gsyxE2i6oq/YpWx23P+OvoANLSMcDzA=="],
|
||||
|
||||
"@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-VpKmJO0xlYcFCRD6JvJlMbNQ6d/6YMHdO1gFIqWlZABHjSSL6BquNNEgWSCv5vdF8ELDBwIYBVZEslakIB/7GA=="],
|
||||
|
||||
"@mediabunny/mp3-encoder": ["@mediabunny/mp3-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-JyzZyGeGRm2HVUQaGJ/VZT9OG+kG00mA8SLP/f3CO7+qWAmBncKc16WYXWHbZEo8Jn/ZGA6De32S5R2bTQbDqA=="],
|
||||
|
||||
"@module-federation/error-codes": ["@module-federation/error-codes@0.22.0", "", {}, "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug=="],
|
||||
|
||||
"@module-federation/runtime": ["@module-federation/runtime@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/runtime-core": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA=="],
|
||||
|
||||
"@module-federation/runtime-core": ["@module-federation/runtime-core@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA=="],
|
||||
|
||||
"@module-federation/runtime-tools": ["@module-federation/runtime-tools@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/webpack-bundler-runtime": "0.22.0" } }, "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA=="],
|
||||
|
||||
"@module-federation/sdk": ["@module-federation/sdk@0.22.0", "", {}, "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g=="],
|
||||
|
||||
"@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
|
||||
|
||||
"@remotion/bundler": ["@remotion/bundler@4.0.483", "", { "dependencies": { "@remotion/media-parser": "4.0.483", "@remotion/studio": "4.0.483", "@remotion/studio-shared": "4.0.483", "@remotion/timeline-utils": "4.0.483", "@rspack/core": "1.7.11", "@rspack/plugin-react-refresh": "1.6.1", "css-loader": "7.1.4", "esbuild": "0.28.1", "react-refresh": "0.18.0", "remotion": "4.0.483", "style-loader": "4.0.0", "webpack": "5.105.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-5fQRMYgr2sxZPBOThnIVuFwHBq/FhusJ+Ke6xopga8io8ecK/mj9GaRXC3tgfMa+YWuu8ZSXZ0U9EsMZjjP5YQ=="],
|
||||
|
||||
"@remotion/canvas-capture": ["@remotion/canvas-capture@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JABgbfvXwjFp1E61tlDN9gVAH8OoeF4kUUELNwG9kcNOE4ex8+vCYEO6VKIaXZ4l8A1PEAQ5EK//NheTJ80gfw=="],
|
||||
|
||||
"@remotion/cli": ["@remotion/cli@4.0.483", "", { "dependencies": { "@remotion/bundler": "4.0.483", "@remotion/media-utils": "4.0.483", "@remotion/player": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio": "4.0.483", "@remotion/studio-server": "4.0.483", "@remotion/studio-shared": "4.0.483", "dotenv": "17.3.1", "minimist": "1.2.6", "prompts": "2.4.2", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "bin": { "remotion": "remotion-cli.js", "remotionb": "remotionb-cli.js", "remotiond": "remotiond-cli.js" } }, "sha512-z4TQcgTfQbSIV4GQpCvIihDL77M+leQelbxv962PmfSKTT7z3XnNN732PcZ4sei2+Xg+d2SD95K4tI5XBYs5Gw=="],
|
||||
|
||||
"@remotion/compositor-darwin-arm64": ["@remotion/compositor-darwin-arm64@4.0.483", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CkEJXouGEoCs5PX3KOufkXrlT/Kgn0ZF7SEQ4meCYGYMZD+QPVtsmS1MXH8pQ4RHlFO6Tk7g0CIHMT3Y4yzpuA=="],
|
||||
|
||||
"@remotion/compositor-darwin-x64": ["@remotion/compositor-darwin-x64@4.0.483", "", { "os": "darwin", "cpu": "x64" }, "sha512-vDZK5U8FYbHGMjFC2lUgJRBz8KXwbwFSvEQi8FATD0roNg75SoaEJWrqAOIVv8v5eBjPvo18znDc36NLRYn1Eg=="],
|
||||
|
||||
"@remotion/compositor-linux-arm64-gnu": ["@remotion/compositor-linux-arm64-gnu@4.0.483", "", { "os": "linux", "cpu": "arm64" }, "sha512-1LzfYDY/DQxy3MIc9pCbTn1ObcBDcyI6MzODOwe1bTZl8TZxdUUt1M3hDrQ60AB6iHwruBAnzT9dnogiMIdSew=="],
|
||||
|
||||
"@remotion/compositor-linux-arm64-musl": ["@remotion/compositor-linux-arm64-musl@4.0.483", "", { "os": "linux", "cpu": "arm64" }, "sha512-nG1btxg0HXuLzulIqAg9NnxWyewZNrA0iLe82HdniaJjeWLORreGo4cEEJz1SjbDpQuPEY562XGkTO2DLKK5mA=="],
|
||||
|
||||
"@remotion/compositor-linux-x64-gnu": ["@remotion/compositor-linux-x64-gnu@4.0.483", "", { "os": "linux", "cpu": "x64" }, "sha512-CV4h6r2rNYh4P8utUzP6RM2vW2vq43nA3IvVOwRSuh9LwJh/7rwG53Oco4HanY/sstaNuymGpT57wdXwONU5CQ=="],
|
||||
|
||||
"@remotion/compositor-linux-x64-musl": ["@remotion/compositor-linux-x64-musl@4.0.483", "", { "os": "linux", "cpu": "x64" }, "sha512-moKvavbkoz5nteJZA4K6LMgTTlewhwIbtO7DYARZ3LK3ClGUKnYfFcmaPS15hMun02JPTOMIWbrlaszdR75mxA=="],
|
||||
|
||||
"@remotion/compositor-win32-x64-msvc": ["@remotion/compositor-win32-x64-msvc@4.0.483", "", { "os": "win32", "cpu": "x64" }, "sha512-eDpWe6Vy7ySHugwxlgaeohqdBwR1etxiymXzT13cgbPw2+p6d55z1tJ7VPTXyfu0o+UdyRTauWqD4msoxN0wow=="],
|
||||
|
||||
"@remotion/licensing": ["@remotion/licensing@4.0.483", "", {}, "sha512-GRtjykrxMmB5Cjpq1oVqeYIRXI95vid6afX0LTLfp1shD6MR4lTZRoRuopxrrZp1E4XFGXoY64b6dlIudgTyBQ=="],
|
||||
|
||||
"@remotion/media-parser": ["@remotion/media-parser@4.0.483", "", {}, "sha512-iva9Eof7QQX+3hGxH/N8pGi80wgdGuzOr825Zn7IuEe7IWdtRYWlFpCEghmyfhrwgaWa7RY51wg77ycR3c3nRg=="],
|
||||
|
||||
"@remotion/media-utils": ["@remotion/media-utils@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4T4WQh/U95kCBzgmz/EL/HL8Zeox/AKAL5ufsehrZFgwlcvH1EsKNJH/RWaAtNPRLF+pskEOgFzPYaczHeI8hg=="],
|
||||
|
||||
"@remotion/player": ["@remotion/player@4.0.483", "", { "dependencies": { "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yyIQ9vinUqqS0fQeL52bPCWQIr8qDXCqQ4iys8P7dSZncdu8V29p4W2a0/4eKV8bv5+mjnJQ/U6nl9NxBF1hdA=="],
|
||||
|
||||
"@remotion/renderer": ["@remotion/renderer@4.0.483", "", { "dependencies": { "@remotion/licensing": "4.0.483", "@remotion/streaming": "4.0.483", "execa": "5.1.1", "remotion": "4.0.483", "source-map": "0.8.0-beta.0", "ws": "8.21.0" }, "optionalDependencies": { "@remotion/compositor-darwin-arm64": "4.0.483", "@remotion/compositor-darwin-x64": "4.0.483", "@remotion/compositor-linux-arm64-gnu": "4.0.483", "@remotion/compositor-linux-arm64-musl": "4.0.483", "@remotion/compositor-linux-x64-gnu": "4.0.483", "@remotion/compositor-linux-x64-musl": "4.0.483", "@remotion/compositor-win32-x64-msvc": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-eSKdQqm8rcl+GNyOhFsBgcPYWMcxTJWHL5xaNrd037piCaL3sdJ57OEO/AbfEvvOO14zwUvlsONKjlkLlEVyKg=="],
|
||||
|
||||
"@remotion/streaming": ["@remotion/streaming@4.0.483", "", {}, "sha512-96rqrk+l5AfriHOqmqxMn8rI6UJfFY7r5UMDZoSKqXWzTOwj+0VWHTRl9ilZQMXhYACaMSA/5yrL9jgQEK8Yuw=="],
|
||||
|
||||
"@remotion/studio": ["@remotion/studio@4.0.483", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@remotion/canvas-capture": "4.0.483", "@remotion/media-utils": "4.0.483", "@remotion/player": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio-shared": "4.0.483", "@remotion/timeline-utils": "4.0.483", "@remotion/web-renderer": "4.0.483", "@remotion/zod-types": "4.0.483", "mediabunny": "1.47.0", "memfs": "3.4.3", "open": "8.4.2", "remotion": "4.0.483", "semver": "7.5.3", "zod": "4.3.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-gNa1Lw8NmHl0jwFb5fk7rzA+WFTJQpGDT/49A38t9jpEwoF+h1mKTgY8gA202apzjBHrEECdFE450npnBOUpUw=="],
|
||||
|
||||
"@remotion/studio-server": ["@remotion/studio-server@4.0.483", "", { "dependencies": { "@babel/parser": "7.24.1", "@babel/types": "7.24.0", "@remotion/bundler": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio-shared": "4.0.483", "memfs": "3.4.3", "open": "8.4.2", "prettier": "3.8.1", "recast": "0.23.11", "remotion": "4.0.483", "semver": "7.5.3" } }, "sha512-2FSLbVN5N8bgtQPX+RQRbis557tJJnVTOZuEBZndfC9NOoPdARlSgEnwVer9tBNczUlc6B3KrPxyPSlS3UjL+Q=="],
|
||||
|
||||
"@remotion/studio-shared": ["@remotion/studio-shared@4.0.483", "", { "dependencies": { "remotion": "4.0.483" } }, "sha512-aXxBYUgsWgphHVq7T7bz+ICMGzfii7pcX1IxS5hbLMLJHqlDtk9g+qcxm4kJ2Fjj/Gb5gERKiNzYym8WZTIM2Q=="],
|
||||
|
||||
"@remotion/timeline-utils": ["@remotion/timeline-utils@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0" } }, "sha512-KTTMdpNA5uRLX+iisAITO+V9cJBk17F8EWvuUnAEVmAFOQTbGIyPI9HNw5pZmL7SlzOJdFjAtksTjipeYGkEug=="],
|
||||
|
||||
"@remotion/web-renderer": ["@remotion/web-renderer@4.0.483", "", { "dependencies": { "@mediabunny/aac-encoder": "1.47.0", "@mediabunny/flac-encoder": "1.47.0", "@mediabunny/mp3-encoder": "1.47.0", "@remotion/licensing": "4.0.483", "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-14iURfVWA/hcF/rJvkid1ZR5nmnVhkTIf071FabCFi6kdViOSVBugSglJ8tUPvWVbWmxlXGDGQD9CSU18IATww=="],
|
||||
|
||||
"@remotion/zod-types": ["@remotion/zod-types@4.0.483", "", { "dependencies": { "remotion": "4.0.483" }, "peerDependencies": { "zod": "4.3.6" } }, "sha512-KIVYIzFpZBDB0wxGGQu5tDBr4XOYRzvrPXjmo6O/FI5Xx7O8hgvJmBs9VDkEwonAOft86sg79hI19F1DTX1p4w=="],
|
||||
|
||||
"@rspack/binding": ["@rspack/binding@1.7.11", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.11", "@rspack/binding-darwin-x64": "1.7.11", "@rspack/binding-linux-arm64-gnu": "1.7.11", "@rspack/binding-linux-arm64-musl": "1.7.11", "@rspack/binding-linux-x64-gnu": "1.7.11", "@rspack/binding-linux-x64-musl": "1.7.11", "@rspack/binding-wasm32-wasi": "1.7.11", "@rspack/binding-win32-arm64-msvc": "1.7.11", "@rspack/binding-win32-ia32-msvc": "1.7.11", "@rspack/binding-win32-x64-msvc": "1.7.11" } }, "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q=="],
|
||||
|
||||
"@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.7.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig=="],
|
||||
|
||||
"@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.7.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA=="],
|
||||
|
||||
"@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA=="],
|
||||
|
||||
"@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw=="],
|
||||
|
||||
"@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ=="],
|
||||
|
||||
"@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg=="],
|
||||
|
||||
"@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.7.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ=="],
|
||||
|
||||
"@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.7.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA=="],
|
||||
|
||||
"@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.7.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw=="],
|
||||
|
||||
"@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.7.11", "", { "os": "win32", "cpu": "x64" }, "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q=="],
|
||||
|
||||
"@rspack/core": ["@rspack/core@1.7.11", "", { "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.11", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew=="],
|
||||
|
||||
"@rspack/lite-tapable": ["@rspack/lite-tapable@1.1.0", "", {}, "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw=="],
|
||||
|
||||
"@rspack/plugin-react-refresh": ["@rspack/plugin-react-refresh@1.6.1", "", { "dependencies": { "error-stack-parser": "^2.1.4", "html-entities": "^2.6.0" }, "peerDependencies": { "react-refresh": ">=0.10.0 <1.0.0", "webpack-hot-middleware": "2.x" }, "optionalPeers": ["webpack-hot-middleware"] }, "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@types/dom-mediacapture-transform": ["@types/dom-mediacapture-transform@0.1.11", "", { "dependencies": { "@types/dom-webcodecs": "*" } }, "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ=="],
|
||||
|
||||
"@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="],
|
||||
|
||||
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
|
||||
|
||||
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
|
||||
|
||||
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
|
||||
|
||||
"@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
|
||||
|
||||
"@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
|
||||
|
||||
"@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
|
||||
|
||||
"@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
|
||||
|
||||
"@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
|
||||
|
||||
"@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
|
||||
|
||||
"@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
|
||||
|
||||
"@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
|
||||
|
||||
"@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
|
||||
|
||||
"@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
|
||||
|
||||
"@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
|
||||
|
||||
"@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
|
||||
|
||||
"@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
|
||||
|
||||
"@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
|
||||
|
||||
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
|
||||
|
||||
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
||||
|
||||
"acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
|
||||
|
||||
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||
|
||||
"ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
|
||||
|
||||
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.40", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.4", "", { "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", "electron-to-chromium": "^1.5.376", "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="],
|
||||
|
||||
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
|
||||
|
||||
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"css-loader": ["css-loader@7.1.4", "", { "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.40", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.6.3" }, "peerDependencies": { "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "webpack": "^5.27.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||
|
||||
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.379", "", {}, "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw=="],
|
||||
|
||||
"error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
|
||||
|
||||
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||
|
||||
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||
|
||||
"fs-monkey": ["fs-monkey@1.0.3", "", {}, "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="],
|
||||
|
||||
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="],
|
||||
|
||||
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
|
||||
"icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="],
|
||||
|
||||
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"loader-runner": ["loader-runner@4.3.2", "", {}, "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w=="],
|
||||
|
||||
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
|
||||
|
||||
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"mediabunny": ["mediabunny@1.47.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-XQMZAcaKPkJ7hQ/Q2fvBdl3ZazQl2WVxDysUbJWh4PuAnLoerdsQBdPTDWdUdK6hh26LQ8Ue94MLLnmpWvlUYg=="],
|
||||
|
||||
"memfs": ["memfs@3.4.3", "", { "dependencies": { "fs-monkey": "1.0.3" } }, "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"minimist": ["minimist@1.2.6", "", {}, "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
|
||||
|
||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
||||
|
||||
"postcss-modules-extract-imports": ["postcss-modules-extract-imports@3.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q=="],
|
||||
|
||||
"postcss-modules-local-by-default": ["postcss-modules-local-by-default@4.2.0", "", { "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw=="],
|
||||
|
||||
"postcss-modules-scope": ["postcss-modules-scope@3.2.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA=="],
|
||||
|
||||
"postcss-modules-values": ["postcss-modules-values@4.0.0", "", { "dependencies": { "icss-utils": "^5.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
|
||||
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||
|
||||
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||
|
||||
"remotion": ["remotion@4.0.483", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-lf4xq4Twn75TQeTavFLrTE4zdck7EKBSx07ZrPv4om40Sist+3G0kq7NNzSjMeJ6G2Wx6vI+0oICD/vv4sGWtg=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
|
||||
|
||||
"semver": ["semver@7.5.3", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||
|
||||
"style-loader": ["style-loader@4.0.0", "", { "peerDependencies": { "webpack": "^5.27.0" } }, "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA=="],
|
||||
|
||||
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"terser": ["terser@5.48.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q=="],
|
||||
|
||||
"terser-webpack-plugin": ["terser-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="],
|
||||
|
||||
"tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"watchpack": ["watchpack@2.5.2", "", { "dependencies": { "graceful-fs": "^4.1.2" } }, "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
|
||||
|
||||
"webpack": ["webpack@5.105.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw=="],
|
||||
|
||||
"webpack-sources": ["webpack-sources@3.5.0", "", {}, "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
|
||||
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"css-loader/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "glm52-rise-video",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"render": "remotion render src/index.tsx GLM52Rise out/glm-52-broke-out.mp4 --codec h264 --pixel-format yuv420p",
|
||||
"render:sheep": "remotion render src/index.tsx NZSheep out/nz-sheep.mp4 --codec h264 --pixel-format yuv420p",
|
||||
"render:novel": "remotion render src/index.tsx NovelTokens out/novel-1984.mp4 --codec h264 --pixel-format yuv420p",
|
||||
"render:flash": "remotion render src/index.tsx FlashShare out/flash-share.mp4 --codec h264 --pixel-format yuv420p",
|
||||
"render:minimax": "remotion render src/index.tsx MiniMaxClimb out/minimax-climb.mp4 --codec h264 --pixel-format yuv420p",
|
||||
"still:june": "remotion still src/index.tsx JuneTotals out/june-totals.png --frame=0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remotion/cli": "^4.0.384",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"remotion": "^4.0.384"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
@@ -0,0 +1,156 @@
|
||||
// Verified against the production opencode-stats PlanetScale DB.
|
||||
// Scope: tier='Go' (OpenCode Go), dataset='zen', client='all', source='all', grain='day'.
|
||||
// metric = total_tokens. Daily token volume per model; GLM-5.2 (zhipu) launched Jun 17.
|
||||
// Segments per day (tokens): glm-5.2, deepseek-v4-flash, deepseek-v4-pro, minimax-m3, all others.
|
||||
|
||||
export type Day = {
|
||||
date: string
|
||||
total: number
|
||||
glm: number
|
||||
dsf: number
|
||||
dsp: number
|
||||
mm: number
|
||||
others: number
|
||||
}
|
||||
|
||||
export const days: Day[] = [
|
||||
{
|
||||
date: "Jun 12",
|
||||
total: 2_283_799_449_383,
|
||||
glm: 0,
|
||||
dsf: 1_176_701_653_509,
|
||||
dsp: 569_527_034_307,
|
||||
mm: 159_016_250_684,
|
||||
others: 378_554_510_883,
|
||||
},
|
||||
{
|
||||
date: "Jun 13",
|
||||
total: 2_008_462_388_420,
|
||||
glm: 0,
|
||||
dsf: 995_338_131_997,
|
||||
dsp: 445_817_536_548,
|
||||
mm: 211_743_241_967,
|
||||
others: 355_563_477_908,
|
||||
},
|
||||
{
|
||||
date: "Jun 14",
|
||||
total: 2_007_785_405_251,
|
||||
glm: 0,
|
||||
dsf: 983_954_176_228,
|
||||
dsp: 428_151_999_341,
|
||||
mm: 262_476_527_930,
|
||||
others: 333_202_701_752,
|
||||
},
|
||||
{
|
||||
date: "Jun 15",
|
||||
total: 2_694_736_103_062,
|
||||
glm: 0,
|
||||
dsf: 1_255_893_953_859,
|
||||
dsp: 632_223_338_376,
|
||||
mm: 352_507_442_991,
|
||||
others: 454_111_367_836,
|
||||
},
|
||||
{
|
||||
date: "Jun 16",
|
||||
total: 2_838_153_758_908,
|
||||
glm: 0,
|
||||
dsf: 1_336_625_283_800,
|
||||
dsp: 676_480_415_730,
|
||||
mm: 305_268_829_013,
|
||||
others: 519_779_230_365,
|
||||
},
|
||||
{
|
||||
date: "Jun 17",
|
||||
total: 2_778_964_711_109,
|
||||
glm: 70_095_977_043,
|
||||
dsf: 1_339_831_523_555,
|
||||
dsp: 660_414_395_220,
|
||||
mm: 251_302_096_157,
|
||||
others: 457_320_719_134,
|
||||
},
|
||||
{
|
||||
date: "Jun 18",
|
||||
total: 2_806_992_430_656,
|
||||
glm: 201_130_231_172,
|
||||
dsf: 1_295_599_996_869,
|
||||
dsp: 595_665_008_776,
|
||||
mm: 322_205_104_324,
|
||||
others: 392_392_089_515,
|
||||
},
|
||||
{
|
||||
date: "Jun 19",
|
||||
total: 2_419_611_630_232,
|
||||
glm: 199_086_413_910,
|
||||
dsf: 1_115_750_468_802,
|
||||
dsp: 475_965_869_304,
|
||||
mm: 303_586_698_735,
|
||||
others: 325_222_179_481,
|
||||
},
|
||||
{
|
||||
date: "Jun 20",
|
||||
total: 2_188_278_916_865,
|
||||
glm: 193_931_516_396,
|
||||
dsf: 1_050_194_681_012,
|
||||
dsp: 395_303_435_278,
|
||||
mm: 281_998_000_337,
|
||||
others: 266_851_283_842,
|
||||
},
|
||||
{
|
||||
date: "Jun 21",
|
||||
total: 2_042_309_961_344,
|
||||
glm: 181_894_043_118,
|
||||
dsf: 985_164_570_580,
|
||||
dsp: 368_194_079_542,
|
||||
mm: 259_812_551_324,
|
||||
others: 247_244_716_780,
|
||||
},
|
||||
{
|
||||
date: "Jun 22",
|
||||
total: 2_893_934_325_663,
|
||||
glm: 301_759_048_475,
|
||||
dsf: 1_298_124_282_989,
|
||||
dsp: 581_012_596_194,
|
||||
mm: 371_581_117_839,
|
||||
others: 341_457_280_166,
|
||||
},
|
||||
{
|
||||
date: "Jun 23",
|
||||
total: 3_109_009_321_480,
|
||||
glm: 282_277_235_158,
|
||||
dsf: 1_423_571_678_821,
|
||||
dsp: 627_374_654_587,
|
||||
mm: 429_416_300_508,
|
||||
others: 346_369_452_406,
|
||||
},
|
||||
{
|
||||
date: "Jun 24",
|
||||
total: 2_939_149_971_595,
|
||||
glm: 256_497_442_533,
|
||||
dsf: 1_373_583_023_234,
|
||||
dsp: 601_270_997_775,
|
||||
mm: 391_586_493_231,
|
||||
others: 316_212_014_822,
|
||||
},
|
||||
{
|
||||
date: "Jun 25",
|
||||
total: 3_029_641_552_948,
|
||||
glm: 256_279_657_734,
|
||||
dsf: 1_481_084_002_776,
|
||||
dsp: 602_077_167_287,
|
||||
mm: 375_985_302_874,
|
||||
others: 314_215_422_277,
|
||||
},
|
||||
]
|
||||
|
||||
export const launchIndex = 5 // Jun 17, first day of GLM-5.2 usage
|
||||
// GLM-5.2 weekly token volume, Jun 19-25 (sum of glm): 1,671,725,357,324 = 1.67T
|
||||
export const glmWeekTokensT = 1.672
|
||||
|
||||
// stacked segments, bottom -> top. GLM-5.2 is the hero (blue); the rest are the field it cut into.
|
||||
export const segments = [
|
||||
{ key: "glm", label: "GLM-5.2", color: "#3b5cf6", hero: true },
|
||||
{ key: "dsf", label: "deepseek-v4-flash", color: "#9ca3ad" },
|
||||
{ key: "dsp", label: "deepseek-v4-pro", color: "#b3b9c1" },
|
||||
{ key: "mm", label: "minimax-m3", color: "#c8cdd3" },
|
||||
{ key: "others", label: "other models", color: "#dde0e4" },
|
||||
] as const
|
||||
@@ -0,0 +1,185 @@
|
||||
import React from "react"
|
||||
import { AbsoluteFill, Easing, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"
|
||||
|
||||
// stats.opencode.ai design tokens (light theme)
|
||||
const c = {
|
||||
bg: "#ffffff",
|
||||
ink: "#161616",
|
||||
muted: "#5c5c5c",
|
||||
faint: "#808080",
|
||||
line: "#e6e6e6",
|
||||
dot: "#e4e4e4",
|
||||
gray: "#aab0b8",
|
||||
accent: "#3b5cf6",
|
||||
accentHi: "#5b78ff",
|
||||
}
|
||||
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
|
||||
const DOT_MASK =
|
||||
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
|
||||
|
||||
// verified: OpenCode Go, week of Jun 22-28, 2026 (2026-W26). share = % of 19.64T total Go tokens.
|
||||
const bars = [
|
||||
{ label: "deepseek-v4-flash", share: 48.3, hero: true },
|
||||
{ label: "deepseek-v4-pro", share: 19.4 },
|
||||
{ label: "minimax-m3", share: 13.0 },
|
||||
{ label: "glm-5.2", share: 8.3 },
|
||||
{ label: "mimo-v2.5", share: 4.3 },
|
||||
{ label: "kimi-k2.7-code", share: 2.6 },
|
||||
{ label: "other models", share: 4.1 },
|
||||
]
|
||||
|
||||
function DataWordmark({ height = 30 }: { height?: number }) {
|
||||
return (
|
||||
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.ink }}>
|
||||
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||
<path
|
||||
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||
<path
|
||||
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function FlashShare() {
|
||||
const frame = useCurrentFrame()
|
||||
const { fps } = useVideoConfig()
|
||||
|
||||
const grow = (i: number) =>
|
||||
Math.min(
|
||||
1,
|
||||
Math.max(0, spring({ frame: frame - 18 - i * 7, fps, config: { damping: 18, stiffness: 120, mass: 0.6 } })),
|
||||
)
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: c.bg, color: c.ink, fontFamily: MONO, padding: 72, boxSizing: "border-box" }}>
|
||||
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<DataWordmark height={30} />
|
||||
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>JUN 22–28, 2026</div>
|
||||
</div>
|
||||
|
||||
{/* headline (static) */}
|
||||
<div style={{ marginTop: 50 }}>
|
||||
<div style={{ fontSize: 23, fontWeight: 600, color: c.muted, letterSpacing: 2 }}>
|
||||
OPENCODE GO · SHARE OF TOKENS
|
||||
</div>
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 24, marginTop: 14 }}
|
||||
>
|
||||
<div style={{ fontSize: 62, fontWeight: 600, letterSpacing: -2, lineHeight: 1 }}>DeepSeek V4 Flash</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 88,
|
||||
fontWeight: 600,
|
||||
letterSpacing: -2,
|
||||
lineHeight: 1,
|
||||
color: c.accent,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
48%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* bar chart */}
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", gap: 16 }}>
|
||||
{bars.map((b, i) => {
|
||||
const g = grow(i)
|
||||
const pct = b.share * g
|
||||
return (
|
||||
<div key={b.label} style={{ display: "flex", alignItems: "center", gap: 18 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 268,
|
||||
fontSize: 23,
|
||||
fontWeight: b.hero ? 600 : 500,
|
||||
color: b.hero ? c.ink : c.muted,
|
||||
textAlign: "right",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{b.label}
|
||||
</div>
|
||||
<div style={{ position: "relative", flex: 1, height: 48 }}>
|
||||
{/* dotted 100% track — height is a multiple of the 12px tile, anchored bottom, so dots never clip */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: c.dot,
|
||||
WebkitMaskImage: DOT_MASK,
|
||||
maskImage: DOT_MASK,
|
||||
WebkitMaskSize: "12px 12px",
|
||||
maskSize: "12px 12px",
|
||||
WebkitMaskRepeat: "repeat",
|
||||
maskRepeat: "repeat",
|
||||
WebkitMaskPosition: "left bottom",
|
||||
maskPosition: "left bottom",
|
||||
}}
|
||||
/>
|
||||
{/* fill */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: `${pct}%`,
|
||||
background: b.hero ? c.accent : c.gray,
|
||||
borderRight: b.hero ? `2px solid ${c.accentHi}` : "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 84,
|
||||
fontSize: 24,
|
||||
fontWeight: b.hero ? 600 : 500,
|
||||
color: b.hero ? c.accent : c.muted,
|
||||
textAlign: "right",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
{Math.round(pct)}%
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* footer */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: 24,
|
||||
paddingTop: 22,
|
||||
borderTop: `1px solid ${c.line}`,
|
||||
fontSize: 20,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "inline-flex", alignItems: "center", gap: 9, color: c.muted }}>
|
||||
<span style={{ width: 13, height: 13, background: c.accent, display: "inline-block" }} />
|
||||
DeepSeek V4 Flash · 9.48T tokens · 83.6M requests
|
||||
</div>
|
||||
<div style={{ color: c.ink }}>opencode.ai/data</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Composition, registerRoot } from "remotion"
|
||||
import { GLM52Rise } from "./video"
|
||||
import { NZSheep } from "./sheep"
|
||||
import { NovelTokens } from "./novel"
|
||||
import { FlashShare } from "./flash"
|
||||
import { MiniMaxClimb } from "./minimax"
|
||||
import { JuneTotals } from "./june"
|
||||
|
||||
function Root() {
|
||||
return (
|
||||
<>
|
||||
<Composition id="GLM52Rise" component={GLM52Rise} durationInFrames={240} fps={30} width={1080} height={1080} />
|
||||
<Composition id="NZSheep" component={NZSheep} durationInFrames={150} fps={30} width={1080} height={1080} />
|
||||
<Composition
|
||||
id="NovelTokens"
|
||||
component={NovelTokens}
|
||||
durationInFrames={150}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1080}
|
||||
/>
|
||||
<Composition id="FlashShare" component={FlashShare} durationInFrames={165} fps={30} width={1080} height={1080} />
|
||||
<Composition
|
||||
id="MiniMaxClimb"
|
||||
component={MiniMaxClimb}
|
||||
durationInFrames={165}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1080}
|
||||
/>
|
||||
<Composition id="JuneTotals" component={JuneTotals} durationInFrames={1} fps={30} width={1080} height={1080} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
registerRoot(Root)
|
||||
@@ -0,0 +1,144 @@
|
||||
import React from "react"
|
||||
import { AbsoluteFill } from "remotion"
|
||||
|
||||
const c = {
|
||||
bg: "#ffffff",
|
||||
ink: "#161616",
|
||||
muted: "#5c5c5c",
|
||||
faint: "#808080",
|
||||
line: "#e6e6e6",
|
||||
dot: "#dcdcdc",
|
||||
accent: "#3b5cf6",
|
||||
}
|
||||
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
|
||||
const DOT_MASK =
|
||||
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
|
||||
|
||||
// verified: OpenCode Go (tier=Go, dataset=zen), June 1-30, 2026.
|
||||
// 72.78T tokens · 651.4M requests · 11.42M sessions -> rounded headline figures.
|
||||
|
||||
function DataWordmark({ height = 30 }: { height?: number }) {
|
||||
return (
|
||||
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.ink }}>
|
||||
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||
<path
|
||||
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||
<path
|
||||
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function DotBand() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: 12,
|
||||
background: c.dot,
|
||||
WebkitMaskImage: DOT_MASK,
|
||||
maskImage: DOT_MASK,
|
||||
WebkitMaskSize: "12px 12px",
|
||||
maskSize: "12px 12px",
|
||||
WebkitMaskRepeat: "repeat",
|
||||
maskRepeat: "repeat",
|
||||
WebkitMaskPosition: "left top",
|
||||
maskPosition: "left top",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Metric({ value, label }: { value: string; label: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 92,
|
||||
fontWeight: 600,
|
||||
letterSpacing: -3,
|
||||
lineHeight: 1,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
marginLeft: -8, // align the glyph's visual left edge to the column
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
<div style={{ marginTop: 16, fontSize: 24, fontWeight: 500, color: c.muted, letterSpacing: 1 }}>{label}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function JuneTotals() {
|
||||
return (
|
||||
<AbsoluteFill style={{ background: c.bg, color: c.ink, fontFamily: MONO, padding: 72, boxSizing: "border-box" }}>
|
||||
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 22 }}>
|
||||
<DataWordmark height={30} />
|
||||
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>MONTHLY RECAP</div>
|
||||
</div>
|
||||
<DotBand />
|
||||
|
||||
{/* hero */}
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center" }}>
|
||||
<div style={{ fontSize: 25, fontWeight: 600, color: c.muted, letterSpacing: 3 }}>OPENCODE GO · JUNE 2026</div>
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 300,
|
||||
fontWeight: 600,
|
||||
letterSpacing: -10,
|
||||
lineHeight: 0.82,
|
||||
color: c.accent,
|
||||
marginLeft: -22,
|
||||
}}
|
||||
>
|
||||
73T
|
||||
</div>
|
||||
<div style={{ fontSize: 50, fontWeight: 600, color: c.ink, marginTop: 10 }}>tokens processed</div>
|
||||
</div>
|
||||
|
||||
{/* supporting metrics */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
marginTop: 60,
|
||||
paddingTop: 40,
|
||||
borderTop: `1px solid ${c.line}`,
|
||||
}}
|
||||
>
|
||||
<Metric value="650M" label="requests" />
|
||||
<Metric value="11M" label="sessions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* footer */}
|
||||
<DotBand />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
marginTop: 22,
|
||||
fontSize: 20,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: c.ink }}>opencode.ai/data</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import React from "react"
|
||||
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"
|
||||
|
||||
const c = {
|
||||
bg: "#ffffff",
|
||||
ink: "#161616",
|
||||
muted: "#5c5c5c",
|
||||
faint: "#808080",
|
||||
line: "#e6e6e6",
|
||||
dot: "#e4e4e4",
|
||||
accent: "#3b5cf6",
|
||||
accentHi: "#5b78ff",
|
||||
accentDim: "#aebcf3",
|
||||
}
|
||||
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
|
||||
const DOT_MASK =
|
||||
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
|
||||
|
||||
// verified: minimax-m3, OpenCode Go (tier=Go, dataset=zen), weekly total_tokens.
|
||||
// W26 2.559T is +23.2% / +482.3B vs W25 2.077T.
|
||||
const weeks = [
|
||||
{ label: "May 25", t: 0.008 },
|
||||
{ label: "Jun 1", t: 0.429 },
|
||||
{ label: "Jun 8", t: 1.192 },
|
||||
{ label: "Jun 15", t: 2.077 },
|
||||
{ label: "Jun 22", t: 2.559, latest: true },
|
||||
]
|
||||
const AXIS_MAX = 3.0
|
||||
|
||||
function DataWordmark({ height = 30 }: { height?: number }) {
|
||||
return (
|
||||
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.ink }}>
|
||||
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||
<path
|
||||
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||
<path
|
||||
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const CHART_H = 420 // multiple of 12 so the dotted tracks never clip
|
||||
|
||||
export function MiniMaxClimb() {
|
||||
const frame = useCurrentFrame()
|
||||
const { fps } = useVideoConfig()
|
||||
|
||||
const grow = (i: number) =>
|
||||
Math.min(
|
||||
1,
|
||||
Math.max(0, spring({ frame: frame - 22 - i * 9, fps, config: { damping: 18, stiffness: 110, mass: 0.6 } })),
|
||||
)
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: c.bg, color: c.ink, fontFamily: MONO, padding: 72, boxSizing: "border-box" }}>
|
||||
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<DataWordmark height={30} />
|
||||
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>JUN 22–28, 2026</div>
|
||||
</div>
|
||||
|
||||
{/* headline (static) */}
|
||||
<div style={{ marginTop: 50 }}>
|
||||
<div style={{ fontSize: 23, fontWeight: 600, color: c.muted, letterSpacing: 2 }}>
|
||||
OPENCODE GO · WEEKLY TOKENS
|
||||
</div>
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 24, marginTop: 14 }}
|
||||
>
|
||||
<div style={{ fontSize: 62, fontWeight: 600, letterSpacing: -2, lineHeight: 1 }}>MiniMax M3</div>
|
||||
<div style={{ textAlign: "right", flexShrink: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 84,
|
||||
fontWeight: 600,
|
||||
letterSpacing: -2,
|
||||
lineHeight: 1,
|
||||
color: c.accent,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
+23.2%
|
||||
</div>
|
||||
<div style={{ fontSize: 19, fontWeight: 500, color: c.muted, letterSpacing: 1, marginTop: 6 }}>
|
||||
WEEK OVER WEEK
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* column chart */}
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "flex-end", marginTop: 30 }}>
|
||||
<div style={{ width: "100%", display: "flex", alignItems: "flex-end", gap: 30 }}>
|
||||
{weeks.map((w, i) => {
|
||||
const g = grow(i)
|
||||
const h = Math.round((w.t / AXIS_MAX) * CHART_H * g)
|
||||
return (
|
||||
<div key={w.label} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||
{/* value + bar */}
|
||||
<div style={{ position: "relative", width: "100%", height: CHART_H }}>
|
||||
{/* dotted track */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: c.dot,
|
||||
WebkitMaskImage: DOT_MASK,
|
||||
maskImage: DOT_MASK,
|
||||
WebkitMaskSize: "12px 12px",
|
||||
maskSize: "12px 12px",
|
||||
WebkitMaskRepeat: "repeat",
|
||||
maskRepeat: "repeat",
|
||||
WebkitMaskPosition: "left bottom",
|
||||
maskPosition: "left bottom",
|
||||
}}
|
||||
/>
|
||||
{/* fill */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: h,
|
||||
background: w.latest ? c.accent : c.accentDim,
|
||||
borderTop: w.latest ? `3px solid ${c.accentHi}` : "none",
|
||||
}}
|
||||
/>
|
||||
{/* value label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: h + 10,
|
||||
textAlign: "center",
|
||||
fontSize: 26,
|
||||
fontWeight: w.latest ? 600 : 500,
|
||||
color: w.latest ? c.accent : c.muted,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
opacity: interpolate(g, [0.5, 1], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{w.t.toFixed(2)}T
|
||||
</div>
|
||||
</div>
|
||||
{/* week label */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 14,
|
||||
fontSize: 18,
|
||||
fontWeight: 500,
|
||||
color: w.latest ? c.ink : c.faint,
|
||||
}}
|
||||
>
|
||||
{w.label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* footer */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: 22,
|
||||
paddingTop: 22,
|
||||
borderTop: `1px solid ${c.line}`,
|
||||
fontSize: 20,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "inline-flex", alignItems: "center", gap: 9, color: c.muted }}>
|
||||
<span style={{ width: 13, height: 13, background: c.accent, display: "inline-block" }} />
|
||||
2.56T tokens last week · +482.3B added
|
||||
</div>
|
||||
<div style={{ color: c.ink }}>opencode.ai/data</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import React from "react"
|
||||
import { AbsoluteFill, Easing, Img, interpolate, staticFile, useCurrentFrame } from "remotion"
|
||||
|
||||
const c = {
|
||||
white: "#ffffff",
|
||||
dim: "rgba(255,255,255,0.74)",
|
||||
}
|
||||
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
|
||||
// verified: OpenCode Go, week of Jun 22-28, 2026 (2026-W26)
|
||||
// 19,642,742,937,105 tokens / 173,651,197 requests = 113,116 tokens/request
|
||||
const AVG = 113116
|
||||
const K = Math.round(AVG / 1000) // 113
|
||||
|
||||
const nf = new Intl.NumberFormat("en-US")
|
||||
|
||||
function DataWordmark({ height = 30 }: { height?: number }) {
|
||||
return (
|
||||
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.white }}>
|
||||
<path opacity="0.35" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||
<path
|
||||
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||
<path
|
||||
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function NovelTokens() {
|
||||
const frame = useCurrentFrame()
|
||||
|
||||
const k = Math.round(
|
||||
K *
|
||||
interpolate(frame, [18, 92], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.out(Easing.cubic),
|
||||
}),
|
||||
)
|
||||
|
||||
const zoom = interpolate(frame, [0, 150], [1.06, 1.12], {
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.inOut(Easing.quad),
|
||||
})
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: "#0c0c0c", overflow: "hidden" }}>
|
||||
<Img
|
||||
src={staticFile("book.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
objectPosition: "center 34%",
|
||||
transform: `scale(${zoom})`,
|
||||
transformOrigin: "center 30%",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* legibility scrims */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background:
|
||||
"linear-gradient(to bottom, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 20%, rgba(0,0,0,0.1) 44%, rgba(0,0,0,0.86) 100%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
boxSizing: "border-box",
|
||||
padding: 64,
|
||||
color: c.white,
|
||||
fontFamily: MONO,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{/* header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<DataWordmark height={30} />
|
||||
<div style={{ fontSize: 20, fontWeight: 500, color: c.dim, letterSpacing: 1 }}>JUN 22–28, 2026</div>
|
||||
</div>
|
||||
|
||||
{/* bottom block */}
|
||||
<div>
|
||||
<div style={{ fontSize: 23, fontWeight: 600, color: c.dim, letterSpacing: 2, marginBottom: 8 }}>
|
||||
OPENCODE GO · LAST WEEK
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 168,
|
||||
fontWeight: 600,
|
||||
lineHeight: 0.92,
|
||||
letterSpacing: -5,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
{k}K
|
||||
</div>
|
||||
<div style={{ fontSize: 50, fontWeight: 600, letterSpacing: -1, marginTop: 4 }}>tokens per request</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: 30,
|
||||
fontSize: 20,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: c.dim }}>{nf.format(AVG)} tokens / request · last week</div>
|
||||
<div style={{ color: c.white }}>opencode.ai/data</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import React from "react"
|
||||
import { AbsoluteFill, Easing, Img, interpolate, staticFile, useCurrentFrame } from "remotion"
|
||||
|
||||
const c = {
|
||||
white: "#ffffff",
|
||||
dim: "rgba(255,255,255,0.72)",
|
||||
faint: "rgba(255,255,255,0.55)",
|
||||
}
|
||||
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
|
||||
// verified: NZ OpenCode Go, week of Jun 22-28, 2026 (2026-W26)
|
||||
const TOKENS = 40_915_594_381 // 40.9B
|
||||
const SHEEP = 23_600_000 // 23.6M
|
||||
const PER_SHEEP = Math.round(TOKENS / SHEEP) // 1,734
|
||||
const nf = new Intl.NumberFormat("en-US")
|
||||
|
||||
// the correct opencode "DATA" wordmark (white, over photo)
|
||||
function DataWordmark({ height = 30 }: { height?: number }) {
|
||||
return (
|
||||
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.white }}>
|
||||
<path opacity="0.35" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||
<path
|
||||
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||
<path
|
||||
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function NZSheep() {
|
||||
const frame = useCurrentFrame()
|
||||
|
||||
const count = Math.round(
|
||||
PER_SHEEP *
|
||||
interpolate(frame, [18, 90], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.out(Easing.cubic),
|
||||
}),
|
||||
)
|
||||
|
||||
// slow Ken Burns push-in (scale up only — never reveals an edge)
|
||||
const zoom = interpolate(frame, [0, 150], [1.06, 1.12], {
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.inOut(Easing.quad),
|
||||
})
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: "#0c0c0c", overflow: "hidden" }}>
|
||||
{/* the sheep, staring */}
|
||||
<Img
|
||||
src={staticFile("sheep.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
objectPosition: "center 38%",
|
||||
transform: `scale(${zoom})`,
|
||||
transformOrigin: "center 35%",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* legibility scrims */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background:
|
||||
"linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 22%, rgba(0,0,0,0) 48%, rgba(0,0,0,0.78) 100%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* content */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
boxSizing: "border-box",
|
||||
padding: 64,
|
||||
color: c.white,
|
||||
fontFamily: MONO,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{/* header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<DataWordmark height={30} />
|
||||
<div style={{ fontSize: 20, fontWeight: 500, color: c.dim, letterSpacing: 1 }}>JUN 22–28, 2026</div>
|
||||
</div>
|
||||
|
||||
{/* bottom block */}
|
||||
<div>
|
||||
<div style={{ fontSize: 23, fontWeight: 600, color: c.dim, letterSpacing: 2, marginBottom: 8 }}>
|
||||
OPENCODE GO · NEW ZEALAND
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 168,
|
||||
fontWeight: 600,
|
||||
lineHeight: 0.92,
|
||||
letterSpacing: -5,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
{nf.format(count)}
|
||||
</div>
|
||||
<div style={{ fontSize: 50, fontWeight: 600, letterSpacing: -1, marginTop: 4 }}>tokens per sheep</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: 34,
|
||||
fontSize: 20,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: c.dim }}>40.9B tokens ÷ 23.6M sheep · last week</div>
|
||||
<div style={{ color: c.white }}>opencode.ai/data</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import React from "react"
|
||||
import { AbsoluteFill, Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion"
|
||||
import { days, launchIndex, glmWeekTokensT, segments } from "./data"
|
||||
|
||||
// stats.opencode.ai design tokens (light theme)
|
||||
const c = {
|
||||
bg: "#ffffff",
|
||||
ink: "#161616",
|
||||
muted: "#5c5c5c",
|
||||
faint: "#808080",
|
||||
line: "#e6e6e6",
|
||||
dot: "#ededed",
|
||||
accent: "#3b5cf6",
|
||||
accentHi: "#5b78ff",
|
||||
}
|
||||
|
||||
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
const W = 1080
|
||||
|
||||
const DOT_MASK =
|
||||
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
|
||||
|
||||
const field = segments.filter((s) => !s.hero)
|
||||
const glmColor = segments.find((s) => s.hero)!.color
|
||||
|
||||
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v))
|
||||
|
||||
// the correct opencode "DATA" wordmark (from stats.opencode.ai header)
|
||||
function DataWordmark({ height = 30, color = c.ink }: { height?: number; color?: string }) {
|
||||
return (
|
||||
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color }}>
|
||||
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
|
||||
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
|
||||
<path
|
||||
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
|
||||
<path
|
||||
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GLM52Rise() {
|
||||
const frame = useCurrentFrame()
|
||||
const { fps } = useVideoConfig()
|
||||
|
||||
// ---------- virtual camera ----------
|
||||
// open zoomed-in on the field, pan right while the blue fills, then pull back to reveal.
|
||||
const K = [0, 32, 150, 206, 240]
|
||||
const ease = Easing.inOut(Easing.cubic)
|
||||
const opt = { extrapolateLeft: "clamp" as const, extrapolateRight: "clamp" as const, easing: ease }
|
||||
const s = interpolate(frame, K, [1.82, 1.72, 1.72, 1.0, 1.0], opt)
|
||||
let fx = interpolate(frame, K, [420, 438, 760, 540, 540], opt)
|
||||
let fy = interpolate(frame, K, [664, 664, 664, 540, 540], opt)
|
||||
// keep the framing inside the 1080 canvas so edges never reveal black
|
||||
fx = clamp(fx, 540 / s, W - 540 / s)
|
||||
fy = clamp(fy, 540 / s, W - 540 / s)
|
||||
const camera = `translate(${540 - fx * s}px, ${540 - fy * s}px) scale(${s})`
|
||||
|
||||
// ---------- blue sweep (synced to the pan) ----------
|
||||
const p = interpolate(frame, [32, 150], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: ease })
|
||||
const revealAmount = p * (days.length - launchIndex) + 0.35
|
||||
const fillOf = (i: number) => clamp(revealAmount - (i - launchIndex), 0, 1)
|
||||
|
||||
// token number climbs as the camera pulls back and the headline re-enters frame
|
||||
const tokensT =
|
||||
glmWeekTokensT *
|
||||
interpolate(frame, [150, 202], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.out(Easing.cubic),
|
||||
})
|
||||
|
||||
// chart geometry
|
||||
const chartH = 440
|
||||
const chartW = 936
|
||||
const gap = 14
|
||||
const EXAGGERATE = 2.876 // broken y-axis: GLM-5.2 magnified ~2.9x for emphasis
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: c.bg, overflow: "hidden" }}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: W,
|
||||
height: W,
|
||||
background: c.bg,
|
||||
transformOrigin: "0 0",
|
||||
transform: camera,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: W,
|
||||
height: W,
|
||||
boxSizing: "border-box",
|
||||
padding: 72,
|
||||
color: c.ink,
|
||||
fontFamily: MONO,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<DataWordmark height={30} />
|
||||
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>JUN 12–25, 2026</div>
|
||||
</div>
|
||||
|
||||
{/* headline (static) */}
|
||||
<div style={{ marginTop: 52 }}>
|
||||
<div style={{ fontSize: 92, fontWeight: 600, lineHeight: 0.98, letterSpacing: -2 }}>
|
||||
GLM-5.2 <span style={{ color: c.accent }}>broke out</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 22, fontSize: 30, fontWeight: 500, color: c.muted }}>
|
||||
From 0 to <span style={{ color: c.ink }}>{tokensT.toFixed(2)}T tokens</span> in a week.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* stacked chart */}
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "flex-end", marginTop: 40 }}>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: chartW,
|
||||
height: chartH,
|
||||
margin: "0 auto",
|
||||
borderBottom: `2px solid ${c.ink}`,
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
{/* faint dotted backdrop */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: -8,
|
||||
right: -8,
|
||||
top: -8,
|
||||
bottom: 0,
|
||||
background: c.dot,
|
||||
WebkitMaskImage: DOT_MASK,
|
||||
maskImage: DOT_MASK,
|
||||
WebkitMaskSize: "12px 12px",
|
||||
maskSize: "12px 12px",
|
||||
WebkitMaskRepeat: "repeat",
|
||||
maskRepeat: "repeat",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* columns */}
|
||||
<div style={{ position: "absolute", inset: 0, display: "flex", gap, alignItems: "flex-end" }}>
|
||||
{days.map((d, i) => {
|
||||
const glmShare = d.glm / d.total
|
||||
const blueH = Math.round(glmShare * EXAGGERATE * chartH)
|
||||
const filled = Math.round(blueH * fillOf(i))
|
||||
const slotGap = filled > 2 ? 5 : 0
|
||||
const fieldH = chartH - filled - slotGap
|
||||
const fieldTotal = d.dsf + d.dsp + d.mm + d.others
|
||||
return (
|
||||
<div key={i} style={{ position: "relative", flex: 1, height: chartH }}>
|
||||
{/* gray field of other models (already in place) */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: fieldH,
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
}}
|
||||
>
|
||||
{field.map((seg) => (
|
||||
<div
|
||||
key={seg.key}
|
||||
style={{
|
||||
height: Math.round(((d[seg.key as keyof typeof d] as number) / fieldTotal) * fieldH),
|
||||
background: seg.color,
|
||||
borderTop: `2px solid ${c.bg}`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* GLM-5.2, animates in */}
|
||||
{filled > 2 && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: filled,
|
||||
background: glmColor,
|
||||
borderTop: `2px solid ${c.accentHi}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* day axis */}
|
||||
<div style={{ display: "flex", gap, width: chartW, margin: "14px auto 0" }}>
|
||||
{days.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlign: "center",
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
color: i === days.length - 1 ? c.ink : c.faint,
|
||||
}}
|
||||
>
|
||||
{i === 0 || i === launchIndex || i === days.length - 1 ? d.date : ""}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* footer */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: 28,
|
||||
paddingTop: 22,
|
||||
borderTop: `1px solid ${c.line}`,
|
||||
fontSize: 20,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "inline-flex", alignItems: "center", gap: 9, color: c.muted }}>
|
||||
<span style={{ width: 13, height: 13, background: c.accent, display: "inline-block" }} />
|
||||
GLM-5.2
|
||||
</div>
|
||||
<div style={{ color: c.ink }}>opencode.ai/data</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
+1
-1
@@ -2,7 +2,7 @@
|
||||
exact = true
|
||||
# Only install newly resolved package versions published at least 3 days ago.
|
||||
minimumReleaseAge = 259200
|
||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@brendonovich/vite-plugin-opencode", "@kitlangton/solid-motion", "@opencode/sdk", "@opencode-ai/pty", "@opencode-ai/pty-darwin-arm64", "@opencode-ai/pty-darwin-x64", "@opencode-ai/pty-linux-arm64-gnu", "@opencode-ai/pty-linux-arm64-musl", "@opencode-ai/pty-linux-x64-gnu", "@opencode-ai/pty-linux-x64-musl", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "@opentunnel/client", "@opentunnel/protocol", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron", "electron-builder", "electron-publish", "blume", "mermaid"]
|
||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@brendonovich/vite-plugin-opencode", "@opencode-ai/sdk", "@opencode-ai/pty", "@opencode-ai/pty-darwin-arm64", "@opencode-ai/pty-darwin-x64", "@opencode-ai/pty-linux-arm64-gnu", "@opencode-ai/pty-linux-arm64-musl", "@opencode-ai/pty-linux-x64-gnu", "@opencode-ai/pty-linux-x64-musl", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish", "blume"]
|
||||
|
||||
[test]
|
||||
root = "./do-not-run-tests-from-root"
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1790510107,
|
||||
"narHash": "sha256-EVMNYv7hYDDD9TGVT/hIyTYgpiXA8y3m5xIEIxuGNU0=",
|
||||
"lastModified": 1776683584,
|
||||
"narHash": "sha256-NuTLMrr10Tng72hurYG8jYQ4XKK8wnpJmOGcPiis96g=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3181085bfd08663b6b9e60bc7a8395c2aaa741bd",
|
||||
"rev": "9dd5558b06dbdacbf635a3dd36dce1b1a7ee3a89",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
forEachSystem = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
|
||||
rev = self.shortRev or self.dirtyShortRev or "dirty";
|
||||
@@ -21,7 +22,7 @@
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
nodejs
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
|
||||
@@ -58,3 +58,12 @@ new sst.cloudflare.x.Astro("Web", {
|
||||
VITE_API_URL: api.url.apply((url) => url!),
|
||||
},
|
||||
})
|
||||
|
||||
new sst.cloudflare.StaticSite("WebApp", {
|
||||
domain: "app." + domain,
|
||||
path: "packages/app",
|
||||
build: {
|
||||
command: "bun turbo build",
|
||||
output: "./dist",
|
||||
},
|
||||
})
|
||||
+174
-21
@@ -1,11 +1,108 @@
|
||||
import { domain } from "./stage"
|
||||
import { deployAws, domain } from "./stage"
|
||||
import { EMAILOCTOPUS_API_KEY } from "./app"
|
||||
import { SECRET } from "./secret"
|
||||
|
||||
const lake = deployAws ? await import("./lake") : undefined
|
||||
|
||||
////////////////
|
||||
// DATABASE
|
||||
////////////////
|
||||
|
||||
const cluster = planetscale.getDatabaseOutput({
|
||||
name: "opencode",
|
||||
organization: "anomalyco",
|
||||
})
|
||||
|
||||
const branch =
|
||||
$app.stage === "production"
|
||||
? planetscale.getBranchOutput({
|
||||
name: "production",
|
||||
organization: cluster.organization,
|
||||
database: cluster.name,
|
||||
})
|
||||
: new planetscale.Branch("DatabaseBranch", {
|
||||
database: cluster.name,
|
||||
organization: cluster.organization,
|
||||
name: $app.stage,
|
||||
parentBranch: "production",
|
||||
})
|
||||
const password = new planetscale.Password("DatabasePassword", {
|
||||
name: $app.stage,
|
||||
database: cluster.name,
|
||||
organization: cluster.organization,
|
||||
branch: branch.name,
|
||||
})
|
||||
|
||||
export const database = new sst.Linkable("Database", {
|
||||
properties: {
|
||||
host: password.accessHostUrl,
|
||||
database: cluster.name,
|
||||
username: password.username,
|
||||
password: password.plaintext,
|
||||
port: 3306,
|
||||
},
|
||||
})
|
||||
|
||||
new sst.x.DevCommand("Studio", {
|
||||
link: [database],
|
||||
dev: {
|
||||
command: "bun db studio",
|
||||
directory: "packages/console/core",
|
||||
autostart: true,
|
||||
},
|
||||
})
|
||||
|
||||
////////////////
|
||||
// AUTH
|
||||
////////////////
|
||||
|
||||
const GITHUB_CLIENT_ID_CONSOLE = new sst.Secret("GITHUB_CLIENT_ID_CONSOLE")
|
||||
const GITHUB_CLIENT_SECRET_CONSOLE = new sst.Secret("GITHUB_CLIENT_SECRET_CONSOLE")
|
||||
const GOOGLE_CLIENT_ID = new sst.Secret("GOOGLE_CLIENT_ID")
|
||||
const authStorage = new sst.cloudflare.Kv("AuthStorage")
|
||||
export const auth = new sst.cloudflare.Worker("AuthApi", {
|
||||
domain: `auth.${domain}`,
|
||||
handler: "packages/console/function/src/auth.ts",
|
||||
url: true,
|
||||
link: [database, authStorage, GITHUB_CLIENT_ID_CONSOLE, GITHUB_CLIENT_SECRET_CONSOLE, GOOGLE_CLIENT_ID],
|
||||
})
|
||||
|
||||
////////////////
|
||||
// GATEWAY
|
||||
////////////////
|
||||
|
||||
export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint", {
|
||||
url: $interpolate`https://${domain}/stripe/webhook`,
|
||||
enabledEvents: [
|
||||
"checkout.session.async_payment_failed",
|
||||
"checkout.session.async_payment_succeeded",
|
||||
"checkout.session.completed",
|
||||
"checkout.session.expired",
|
||||
"charge.refunded",
|
||||
"invoice.payment_succeeded",
|
||||
"invoice.payment_failed",
|
||||
"invoice.payment_action_required",
|
||||
"customer.created",
|
||||
"customer.deleted",
|
||||
"customer.updated",
|
||||
"customer.discount.created",
|
||||
"customer.discount.deleted",
|
||||
"customer.discount.updated",
|
||||
"customer.source.created",
|
||||
"customer.source.deleted",
|
||||
"customer.source.expiring",
|
||||
"customer.source.updated",
|
||||
"customer.subscription.created",
|
||||
"customer.subscription.deleted",
|
||||
"customer.subscription.paused",
|
||||
"customer.subscription.pending_update_applied",
|
||||
"customer.subscription.pending_update_expired",
|
||||
"customer.subscription.resumed",
|
||||
"customer.subscription.trial_will_end",
|
||||
"customer.subscription.updated",
|
||||
],
|
||||
})
|
||||
|
||||
const zenLiteProduct = new stripe.Product("ZenLite", {
|
||||
name: "OpenCode Go",
|
||||
})
|
||||
@@ -53,9 +150,8 @@ const zenLitePrice = new stripe.Price("ZenLitePrice", {
|
||||
})
|
||||
const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
|
||||
properties: {
|
||||
// Use existing Go resources in dev's checkout Stripe account.
|
||||
product: $app.stage === "dev" ? "prod_U1tUscpmwtV2bG" : zenLiteProduct.id,
|
||||
price: $app.stage === "dev" ? "price_1T3phhE7fOCwHSD4zS6w2NPy" : zenLitePrice.id,
|
||||
product: zenLiteProduct.id,
|
||||
price: zenLitePrice.id,
|
||||
priceInr: 92900,
|
||||
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
||||
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
|
||||
@@ -88,28 +184,57 @@ const ZEN_BLACK_PRICE = new sst.Linkable("ZEN_BLACK_PRICE", {
|
||||
},
|
||||
})
|
||||
|
||||
// Preview stages have no new Console or inference deployment to forward to.
|
||||
const migrationDomain =
|
||||
$app.stage === "production" ? "opencode.ai" : $app.stage === "dev" ? "dev.opencode.ai" : undefined
|
||||
const consoleMigration = new sst.Linkable("ConsoleMigration", {
|
||||
properties: {
|
||||
consoleUrl: migrationDomain ? `https://${migrationDomain}/console` : "",
|
||||
inferenceUrl: migrationDomain ? `https://${migrationDomain}/inference` : "",
|
||||
},
|
||||
const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS1"),
|
||||
new sst.Secret("ZEN_MODELS2"),
|
||||
new sst.Secret("ZEN_MODELS3"),
|
||||
new sst.Secret("ZEN_MODELS4"),
|
||||
new sst.Secret("ZEN_MODELS5"),
|
||||
new sst.Secret("ZEN_MODELS6"),
|
||||
new sst.Secret("ZEN_MODELS7"),
|
||||
new sst.Secret("ZEN_MODELS8"),
|
||||
new sst.Secret("ZEN_MODELS9"),
|
||||
new sst.Secret("ZEN_MODELS10"),
|
||||
new sst.Secret("ZEN_MODELS11"),
|
||||
new sst.Secret("ZEN_MODELS12"),
|
||||
new sst.Secret("ZEN_MODELS13"),
|
||||
new sst.Secret("ZEN_MODELS14"),
|
||||
new sst.Secret("ZEN_MODELS15"),
|
||||
new sst.Secret("ZEN_MODELS16"),
|
||||
new sst.Secret("ZEN_MODELS17"),
|
||||
new sst.Secret("ZEN_MODELS18"),
|
||||
new sst.Secret("ZEN_MODELS19"),
|
||||
new sst.Secret("ZEN_MODELS20"),
|
||||
new sst.Secret("ZEN_MODELS21"),
|
||||
new sst.Secret("ZEN_MODELS22"),
|
||||
new sst.Secret("ZEN_MODELS23"),
|
||||
new sst.Secret("ZEN_MODELS24"),
|
||||
new sst.Secret("ZEN_MODELS25"),
|
||||
new sst.Secret("ZEN_MODELS26"),
|
||||
new sst.Secret("ZEN_MODELS27"),
|
||||
new sst.Secret("ZEN_MODELS28"),
|
||||
new sst.Secret("ZEN_MODELS29"),
|
||||
new sst.Secret("ZEN_MODELS30"),
|
||||
]
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
properties: { value: auth.url.apply((url) => url!) },
|
||||
})
|
||||
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
||||
properties: { value: stripeWebhook.secret },
|
||||
})
|
||||
|
||||
////////////////
|
||||
// CONSOLE
|
||||
////////////////
|
||||
|
||||
// Archived Zen request/response dumps (writer removed May 2026). Retained on purpose; nothing reads or writes them.
|
||||
new sst.cloudflare.Bucket("ZenData")
|
||||
new sst.cloudflare.Bucket("ZenDataNew")
|
||||
const bucket = new sst.cloudflare.Bucket("ZenData")
|
||||
const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
||||
|
||||
const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL")
|
||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||
const ENTERPRISE_SALES_INBOX_EMAIL = new sst.Secret("ENTERPRISE_SALES_INBOX_EMAIL")
|
||||
|
||||
const SALESFORCE_CLIENT_ID = new sst.Secret("SALESFORCE_CLIENT_ID")
|
||||
const SALESFORCE_CLIENT_SECRET = new sst.Secret("SALESFORCE_CLIENT_SECRET")
|
||||
@@ -117,29 +242,47 @@ const SALESFORCE_INSTANCE_URL = new sst.Secret("SALESFORCE_INSTANCE_URL")
|
||||
|
||||
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||
handler: "packages/console/function/src/log-processor.ts",
|
||||
link: [SECRET.HoneycombApiKey],
|
||||
link: [SECRET.HoneycombApiKey, ...(lake?.lakeIngest ? [lake.lakeIngest] : [])],
|
||||
})
|
||||
|
||||
new sst.cloudflare.x.SolidStart("Console", {
|
||||
domain,
|
||||
path: "packages/console/app",
|
||||
link: [
|
||||
consoleMigration,
|
||||
bucket,
|
||||
bucketNew,
|
||||
database,
|
||||
SECRET.UpstashRedisRestUrl,
|
||||
SECRET.UpstashRedisRestToken,
|
||||
AUTH_API_URL,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
SECRET.SupportApiKey,
|
||||
DISCORD_INCIDENT_WEBHOOK_URL,
|
||||
SECRET.HoneycombWebhookSecret,
|
||||
STRIPE_SECRET_KEY,
|
||||
EMAILOCTOPUS_API_KEY,
|
||||
AWS_SES_ACCESS_KEY_ID,
|
||||
AWS_SES_SECRET_ACCESS_KEY,
|
||||
ENTERPRISE_SALES_INBOX_EMAIL,
|
||||
SALESFORCE_CLIENT_ID,
|
||||
SALESFORCE_CLIENT_SECRET,
|
||||
SALESFORCE_INSTANCE_URL,
|
||||
new sst.Secret("CLOUDFLARE_ACCESS_CLIENT_ID"),
|
||||
ZEN_BLACK_PRICE,
|
||||
ZEN_LITE_PRICE,
|
||||
new sst.Secret("ZEN_LIMITS"),
|
||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||
...ZEN_MODELS,
|
||||
...($dev
|
||||
? [
|
||||
new sst.Secret("CLOUDFLARE_DEFAULT_ACCOUNT_ID", process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID!),
|
||||
new sst.Secret("CLOUDFLARE_API_TOKEN", process.env.CLOUDFLARE_API_TOKEN!),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
environment: {
|
||||
//VITE_DOCS_URL: web.url.apply((url) => url!),
|
||||
//VITE_API_URL: gateway.url.apply((url) => url!),
|
||||
VITE_CONSOLE_MARKETING_ENABLED: $app.stage === "dev" ? "true" : "false",
|
||||
VITE_AUTH_URL: auth.url.apply((url) => url!),
|
||||
VITE_STRIPE_PUBLISHABLE_KEY: STRIPE_PUBLISHABLE_KEY.value,
|
||||
},
|
||||
transform: {
|
||||
server: {
|
||||
@@ -156,3 +299,13 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
////////////////
|
||||
// HELPERS
|
||||
////////////////
|
||||
|
||||
export const stat = new sst.cloudflare.Worker("Stat", {
|
||||
handler: "packages/console/function/src/stat.ts",
|
||||
link: [database],
|
||||
url: true,
|
||||
})
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
import { domain } from "./stage"
|
||||
|
||||
const current = aws.getCallerIdentityOutput({})
|
||||
const partition = aws.getPartitionOutput({})
|
||||
const region = aws.getRegionOutput({})
|
||||
|
||||
const tableBucketName = `opencode-${$app.stage}-lake`
|
||||
const glueCatalogName = "s3tablescatalog"
|
||||
const glueCatalogArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:catalog`
|
||||
const glueS3TablesCatalogArn = $interpolate`${glueCatalogArn}/${glueCatalogName}`
|
||||
const glueS3TablesChildCatalogArn = $interpolate`${glueS3TablesCatalogArn}/${tableBucketName}`
|
||||
const glueS3TablesDatabaseWildcardArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/${glueCatalogName}/${tableBucketName}/*`
|
||||
const glueS3TablesTableWildcardArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/${tableBucketName}/*/*`
|
||||
const s3TablesBucketWildcardArn = $interpolate`arn:${partition.partition}:s3tables:${region.region}:${current.accountId}:bucket/*`
|
||||
|
||||
export const tableBucket = new aws.s3tables.TableBucket("LakeTableBucket", {
|
||||
name: tableBucketName,
|
||||
forceDestroy: $app.stage !== "production",
|
||||
})
|
||||
|
||||
const s3TablesCatalog = new aws.cloudcontrol.Resource(
|
||||
"LakeS3TablesCatalog",
|
||||
{
|
||||
typeName: "AWS::Glue::Catalog",
|
||||
desiredState: $jsonStringify({
|
||||
Name: glueCatalogName,
|
||||
Description: "Federated catalog for S3 Tables",
|
||||
FederatedCatalog: {
|
||||
Identifier: s3TablesBucketWildcardArn,
|
||||
ConnectionName: "aws:s3tables",
|
||||
},
|
||||
CreateDatabaseDefaultPermissions: [
|
||||
{
|
||||
Principal: {
|
||||
DataLakePrincipalIdentifier: "IAM_ALLOWED_PRINCIPALS",
|
||||
},
|
||||
Permissions: ["ALL"],
|
||||
},
|
||||
],
|
||||
CreateTableDefaultPermissions: [
|
||||
{
|
||||
Principal: {
|
||||
DataLakePrincipalIdentifier: "IAM_ALLOWED_PRINCIPALS",
|
||||
},
|
||||
Permissions: ["ALL"],
|
||||
},
|
||||
],
|
||||
AllowFullTableExternalDataAccess: "True",
|
||||
}),
|
||||
},
|
||||
{ dependsOn: [tableBucket] },
|
||||
)
|
||||
|
||||
const athenaResultsBucket = new aws.s3.Bucket("LakeAthenaResults", {
|
||||
bucket: `opencode-${$app.stage}-lake-athena-results`,
|
||||
forceDestroy: $app.stage !== "production",
|
||||
})
|
||||
|
||||
const firehoseErrorBucket = new aws.s3.Bucket("LakeFirehoseErrors", {
|
||||
bucket: `opencode-${$app.stage}-lake-firehose-errors`,
|
||||
forceDestroy: $app.stage !== "production",
|
||||
})
|
||||
|
||||
const athenaWorkgroup = new aws.athena.Workgroup("LakeAthenaWorkgroup", {
|
||||
name: `opencode-${$app.stage}-lake-workgroup`,
|
||||
forceDestroy: $app.stage !== "production",
|
||||
configuration: {
|
||||
enforceWorkgroupConfiguration: true,
|
||||
publishCloudwatchMetricsEnabled: true,
|
||||
// Athena bills $5/TB scanned; kill any query that would scan more than 2 TB
|
||||
// so a regression cannot silently burn money. Stats sync full passes scan
|
||||
// ~250 GB as of 2026-07.
|
||||
bytesScannedCutoffPerQuery: 2 * 1024 ** 4,
|
||||
resultConfiguration: {
|
||||
outputLocation: $interpolate`s3://${athenaResultsBucket.bucket}/`,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const firehoseRole = new aws.iam.Role("LakeFirehoseRole", {
|
||||
assumeRolePolicy: aws.iam.getPolicyDocumentOutput({
|
||||
statements: [
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: ["sts:AssumeRole"],
|
||||
principals: [
|
||||
{
|
||||
type: "Service",
|
||||
identifiers: ["firehose.amazonaws.com"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).json,
|
||||
})
|
||||
|
||||
const firehosePolicy = new aws.iam.RolePolicy("LakeFirehosePolicy", {
|
||||
role: firehoseRole.id,
|
||||
policy: aws.iam.getPolicyDocumentOutput({
|
||||
statements: [
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: [
|
||||
"s3tables:ListTableBuckets",
|
||||
"s3tables:GetTableBucket",
|
||||
"s3tables:GetNamespace",
|
||||
"s3tables:GetTable",
|
||||
"s3tables:GetTableData",
|
||||
"s3tables:GetTableMetadataLocation",
|
||||
"s3tables:ListNamespaces",
|
||||
"s3tables:ListTables",
|
||||
"s3tables:PutTableData",
|
||||
"s3tables:UpdateTableMetadataLocation",
|
||||
],
|
||||
resources: ["*"],
|
||||
},
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: [
|
||||
"glue:GetCatalog",
|
||||
"glue:GetCatalogs",
|
||||
"glue:GetDatabase",
|
||||
"glue:GetDatabases",
|
||||
"glue:GetTable",
|
||||
"glue:GetTables",
|
||||
"glue:UpdateTable",
|
||||
],
|
||||
resources: [
|
||||
glueCatalogArn,
|
||||
glueS3TablesCatalogArn,
|
||||
$interpolate`${glueS3TablesCatalogArn}/*`,
|
||||
glueS3TablesDatabaseWildcardArn,
|
||||
glueS3TablesTableWildcardArn,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/*`,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/*/*`,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/*`,
|
||||
],
|
||||
},
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: [
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:GetBucketLocation",
|
||||
"s3:GetObject",
|
||||
"s3:ListBucket",
|
||||
"s3:ListBucketMultipartUploads",
|
||||
"s3:PutObject",
|
||||
],
|
||||
resources: [firehoseErrorBucket.arn, $interpolate`${firehoseErrorBucket.arn}/*`],
|
||||
},
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: ["lakeformation:GetDataAccess"],
|
||||
resources: ["*"],
|
||||
},
|
||||
],
|
||||
}).json,
|
||||
})
|
||||
|
||||
const firehose = new aws.kinesis.FirehoseDeliveryStream(
|
||||
"LakeFirehose",
|
||||
{
|
||||
name: `opencode-${$app.stage}-lake-ingest`,
|
||||
destination: "iceberg",
|
||||
icebergConfiguration: {
|
||||
appendOnly: true,
|
||||
bufferingInterval: 60,
|
||||
bufferingSize: 1,
|
||||
catalogArn: glueS3TablesChildCatalogArn,
|
||||
processingConfiguration: {
|
||||
enabled: true,
|
||||
processors: [
|
||||
{
|
||||
type: "MetadataExtraction",
|
||||
parameters: [
|
||||
{ parameterName: "JsonParsingEngine", parameterValue: "JQ-1.6" },
|
||||
{
|
||||
parameterName: "MetadataExtractionQuery",
|
||||
parameterValue:
|
||||
'{destinationDatabaseName:._lake_database,destinationTableName:._lake_table,operation:(._lake_operation // "insert")}',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
roleArn: firehoseRole.arn,
|
||||
s3BackupMode: "FailedDataOnly",
|
||||
s3Configuration: {
|
||||
roleArn: firehoseRole.arn,
|
||||
bucketArn: firehoseErrorBucket.arn,
|
||||
errorOutputPrefix: "errors/!{firehose:error-output-type}/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dependsOn: [s3TablesCatalog, firehosePolicy] },
|
||||
)
|
||||
|
||||
export const lakeVpc = new sst.aws.Vpc("LakeVpc")
|
||||
export const lakeCluster = new sst.aws.Cluster("LakeCluster", { vpc: lakeVpc })
|
||||
export const lakeRegion = region.region
|
||||
export const lakeCatalog = $interpolate`${glueCatalogName}/${tableBucket.name}`
|
||||
export const lakeAthenaWorkgroup = athenaWorkgroup
|
||||
|
||||
const ingestSecret = new random.RandomPassword("LakeIngestSecret", { length: 32 })
|
||||
export const ingestSecretSsm = new aws.ssm.Parameter("LakeIngestSecretSsm", {
|
||||
name: $interpolate`/${$app.name}/${$app.stage}/lake/ingest/secret`,
|
||||
type: "SecureString",
|
||||
value: ingestSecret.result,
|
||||
})
|
||||
|
||||
const ingestConfig = new sst.Linkable("LakeIngestConfig", {
|
||||
properties: {
|
||||
streamName: firehose.name,
|
||||
secret: ingestSecret.result,
|
||||
},
|
||||
})
|
||||
|
||||
const ingestService = new sst.aws.Service("LakeIngestService", {
|
||||
cluster: lakeCluster,
|
||||
architecture: "arm64",
|
||||
cpu: "1 vCPU",
|
||||
memory: "4 GB",
|
||||
image: {
|
||||
context: ".",
|
||||
dockerfile: "packages/stats/server/Dockerfile",
|
||||
},
|
||||
link: [ingestConfig],
|
||||
permissions: [
|
||||
{
|
||||
actions: ["firehose:PutRecord", "firehose:PutRecordBatch"],
|
||||
resources: [firehose.arn],
|
||||
},
|
||||
],
|
||||
scaling: {
|
||||
min: $app.stage === "production" ? 2 : 1,
|
||||
max: $app.stage === "production" ? 32 : 4,
|
||||
cpuUtilization: 60,
|
||||
memoryUtilization: 70,
|
||||
},
|
||||
loadBalancer: {
|
||||
domain: {
|
||||
name: `lake.${domain}`,
|
||||
dns: sst.cloudflare.dns(),
|
||||
},
|
||||
rules: [
|
||||
{ listen: "80/http", redirect: "443/https" },
|
||||
{ listen: "443/https", forward: "3000/http" },
|
||||
],
|
||||
health: {
|
||||
"3000/http": {
|
||||
path: "/ready",
|
||||
successCodes: "200-299",
|
||||
},
|
||||
},
|
||||
},
|
||||
health: {
|
||||
command: [
|
||||
"CMD-SHELL",
|
||||
"bun --eval \"fetch('http://localhost:3000/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\"",
|
||||
],
|
||||
interval: "30 seconds",
|
||||
retries: 3,
|
||||
startPeriod: "30 seconds",
|
||||
timeout: "5 seconds",
|
||||
},
|
||||
dev: {
|
||||
command: "bun run start",
|
||||
directory: "packages/stats/server",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
wait: $app.stage === "production",
|
||||
})
|
||||
|
||||
export const lakeIngest = new sst.Linkable("LakeIngest", {
|
||||
properties: {
|
||||
url: ingestService.url,
|
||||
secret: ingestSecret.result,
|
||||
},
|
||||
})
|
||||
|
||||
export const lakeQueryPermissions = [
|
||||
{
|
||||
actions: ["athena:StartQueryExecution", "athena:GetQueryExecution", "athena:GetQueryResults"],
|
||||
resources: [athenaWorkgroup.arn],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"glue:GetCatalog",
|
||||
"glue:GetCatalogs",
|
||||
"glue:GetDatabase",
|
||||
"glue:GetDatabases",
|
||||
"glue:GetTable",
|
||||
"glue:GetTables",
|
||||
"glue:GetPartitions",
|
||||
],
|
||||
resources: [
|
||||
glueCatalogArn,
|
||||
glueS3TablesCatalogArn,
|
||||
$interpolate`${glueS3TablesCatalogArn}/*`,
|
||||
glueS3TablesDatabaseWildcardArn,
|
||||
glueS3TablesTableWildcardArn,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/*`,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/*/*`,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/*`,
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: ["s3:GetBucketLocation", "s3:ListBucket"],
|
||||
resources: [athenaResultsBucket.arn],
|
||||
},
|
||||
{
|
||||
actions: ["s3:GetObject", "s3:PutObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads"],
|
||||
resources: [$interpolate`${athenaResultsBucket.arn}/*`],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"s3tables:GetTableBucket",
|
||||
"s3tables:GetNamespace",
|
||||
"s3tables:GetTable",
|
||||
"s3tables:GetTableData",
|
||||
"s3tables:GetTableMetadataLocation",
|
||||
"s3tables:ListNamespaces",
|
||||
"s3tables:ListTables",
|
||||
],
|
||||
resources: ["*"],
|
||||
},
|
||||
{
|
||||
actions: ["lakeformation:GetDataAccess"],
|
||||
resources: ["*"],
|
||||
},
|
||||
]
|
||||
@@ -10,4 +10,6 @@ export const SECRET = {
|
||||
HoneycombApiKey: new sst.Secret("HONEYCOMB_API_KEY"),
|
||||
HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }),
|
||||
SupportApiKey: new sst.Secret("SUPPORT_API_KEY"),
|
||||
UpstashRedisRestUrl: new sst.Secret("UpstashRedisRestUrl"),
|
||||
UpstashRedisRestToken: new sst.Secret("UpstashRedisRestToken"),
|
||||
}
|
||||
+100
-19
@@ -1,10 +1,104 @@
|
||||
import { lakeAthenaWorkgroup, lakeCatalog, lakeCluster, lakeQueryPermissions, lakeRegion, tableBucket } from "./lake"
|
||||
import { EMAILOCTOPUS_API_KEY } from "./app"
|
||||
import { domain } from "./stage"
|
||||
|
||||
// Keep the deployed names and arguments: the R2 stats sync still uses this VPC
|
||||
// and cluster after the legacy lake is removed.
|
||||
const lakeVpc = new sst.aws.Vpc("LakeVpc")
|
||||
const lakeCluster = new sst.aws.Cluster("LakeCluster", { vpc: lakeVpc })
|
||||
////////////////
|
||||
// LAKE
|
||||
////////////////
|
||||
|
||||
const inferenceNamespace = new aws.s3tables.Namespace("LakeInferenceNamespace", {
|
||||
namespace: "inference",
|
||||
tableBucketArn: tableBucket.arn,
|
||||
})
|
||||
|
||||
const inferenceEventTable = new aws.s3tables.Table(
|
||||
"LakeInferenceEventTable",
|
||||
{
|
||||
name: "event",
|
||||
namespace: inferenceNamespace.namespace,
|
||||
tableBucketArn: inferenceNamespace.tableBucketArn,
|
||||
format: "ICEBERG",
|
||||
metadata: {
|
||||
iceberg: {
|
||||
schema: {
|
||||
fields: [
|
||||
{ name: "event_timestamp", type: "string", required: false },
|
||||
{ name: "event_date", type: "string", required: false },
|
||||
{ name: "event_type", type: "string", required: false },
|
||||
{ name: "dataset", type: "string", required: false },
|
||||
{ name: "cf_continent", type: "string", required: false },
|
||||
{ name: "cf_country", type: "string", required: false },
|
||||
{ name: "cf_city", type: "string", required: false },
|
||||
{ name: "cf_region", type: "string", required: false },
|
||||
{ name: "cf_latitude", type: "double", required: false },
|
||||
{ name: "cf_longitude", type: "double", required: false },
|
||||
{ name: "cf_timezone", type: "string", required: false },
|
||||
{ name: "duration", type: "double", required: false },
|
||||
{ name: "request_length", type: "long", required: false },
|
||||
{ name: "status", type: "int", required: false },
|
||||
{ name: "ip", type: "string", required: false },
|
||||
{ name: "is_stream", type: "boolean", required: false },
|
||||
{ name: "session", type: "string", required: false },
|
||||
{ name: "request", type: "string", required: false },
|
||||
{ name: "client", type: "string", required: false },
|
||||
{ name: "user_agent", type: "string", required: false },
|
||||
{ name: "model", type: "string", required: false },
|
||||
{ name: "model_tier", type: "string", required: false },
|
||||
{ name: "model_variant", type: "string", required: false },
|
||||
{ name: "source", type: "string", required: false },
|
||||
{ name: "provider", type: "string", required: false },
|
||||
{ name: "provider_model", type: "string", required: false },
|
||||
{ name: "llm_error_code", type: "int", required: false },
|
||||
{ name: "llm_error_message", type: "string", required: false },
|
||||
{ name: "error_response", type: "string", required: false },
|
||||
{ name: "error_type", type: "string", required: false },
|
||||
{ name: "error_message", type: "string", required: false },
|
||||
{ name: "error_cause", type: "string", required: false },
|
||||
{ name: "error_cause2", type: "string", required: false },
|
||||
{ name: "api_key", type: "string", required: false },
|
||||
{ name: "workspace", type: "string", required: false },
|
||||
{ name: "user_id", type: "string", required: false },
|
||||
{ name: "is_subscription", type: "boolean", required: false },
|
||||
{ name: "subscription", type: "string", required: false },
|
||||
{ name: "response_length", type: "long", required: false },
|
||||
{ name: "time_to_first_byte", type: "long", required: false },
|
||||
{ name: "timestamp_first_byte", type: "long", required: false },
|
||||
{ name: "timestamp_last_byte", type: "long", required: false },
|
||||
{ name: "tokens_input", type: "long", required: false },
|
||||
{ name: "tokens_output", type: "long", required: false },
|
||||
{ name: "tokens_reasoning", type: "long", required: false },
|
||||
{ name: "tokens_cache_read", type: "long", required: false },
|
||||
{ name: "tokens_cache_write_5m", type: "long", required: false },
|
||||
{ name: "tokens_cache_write_1h", type: "long", required: false },
|
||||
{ name: "cost_input_microcents", type: "long", required: false },
|
||||
{ name: "cost_output_microcents", type: "long", required: false },
|
||||
{ name: "cost_cache_read_microcents", type: "long", required: false },
|
||||
{ name: "cost_cache_write_microcents", type: "long", required: false },
|
||||
{ name: "cost_total_microcents", type: "long", required: false },
|
||||
{ name: "cost_input", type: "long", required: false },
|
||||
{ name: "cost_output", type: "long", required: false },
|
||||
{ name: "cost_cache_read", type: "long", required: false },
|
||||
{ name: "cost_cache_write_5m", type: "long", required: false },
|
||||
{ name: "cost_cache_write_1h", type: "long", required: false },
|
||||
{ name: "cost_total", type: "long", required: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ deleteBeforeReplace: $app.stage !== "production", ignoreChanges: ["metadata"] },
|
||||
)
|
||||
|
||||
export const inferenceEvent = new sst.Linkable("InferenceEvent", {
|
||||
properties: {
|
||||
region: lakeRegion,
|
||||
catalog: lakeCatalog,
|
||||
database: inferenceNamespace.namespace,
|
||||
table: inferenceEventTable.name,
|
||||
tableBucket: tableBucket.name,
|
||||
workgroup: lakeAthenaWorkgroup.name,
|
||||
},
|
||||
})
|
||||
|
||||
////////////////
|
||||
// DATABASE
|
||||
@@ -87,20 +181,6 @@ const statsSyncConfig = new sst.Linkable("StatsSyncConfig", {
|
||||
},
|
||||
})
|
||||
|
||||
// Comma-separated model IDs kept off the public stats site. A secret, so excluding
|
||||
// an unreleased model does not publish its name in git.
|
||||
const statsHiddenModels = new sst.Secret("StatsHiddenModels", "")
|
||||
|
||||
const r2SqlAuthToken = new sst.Secret("R2SqlAuthToken")
|
||||
const r2Sql = new sst.Linkable("R2Sql", {
|
||||
properties: {
|
||||
accountId: "15d29c8639fd3733b1b5486a2acfd968",
|
||||
bucket: `platform-${$app.stage}-lake`,
|
||||
namespace: "inference",
|
||||
table: "generation",
|
||||
},
|
||||
})
|
||||
|
||||
export const statSync = new sst.aws.Service("StatsSyncService", {
|
||||
cluster: lakeCluster,
|
||||
architecture: "arm64",
|
||||
@@ -113,7 +193,8 @@ export const statSync = new sst.aws.Service("StatsSyncService", {
|
||||
dockerfile: "packages/stats/server/Dockerfile",
|
||||
},
|
||||
command: ["bun", "src/stat-sync.ts"],
|
||||
link: [database, r2Sql, r2SqlAuthToken, statsHiddenModels, statsSyncConfig],
|
||||
link: [database, inferenceEvent, statsSyncConfig],
|
||||
permissions: lakeQueryPermissions,
|
||||
scaling: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
export function createWebApp(domain: string) {
|
||||
return new sst.cloudflare.StaticSite("WebApp", {
|
||||
domain,
|
||||
path: "packages/app",
|
||||
environment:
|
||||
$app.stage === "beta"
|
||||
? {
|
||||
OPENCODE_CHANNEL: "beta",
|
||||
VITE_OPENCODE_SERVER_MODE: "none",
|
||||
VITE_SENTRY_ENVIRONMENT: "beta",
|
||||
}
|
||||
: undefined,
|
||||
build: {
|
||||
// Preserve Sentry credentials and run source-map uploads on every deployment.
|
||||
command: "bun run build",
|
||||
output: "./dist",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
APP=opencode
|
||||
SOURCE_APP=opencode
|
||||
APP=opencode2
|
||||
|
||||
MUTED='\033[0;2m'
|
||||
RED='\033[0;31m'
|
||||
@@ -23,7 +22,7 @@ Options:
|
||||
Examples:
|
||||
curl -fsSL https://opencode.ai/v2/install | bash
|
||||
curl -fsSL https://opencode.ai/v2/install | bash -s -- --version 0.0.0-beta-17236
|
||||
./install --binary /path/to/opencode
|
||||
./install --binary /path/to/opencode2
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -166,30 +165,22 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
package_scope="@opencode"
|
||||
if [ -z "$requested_version" ]; then
|
||||
metadata=$(curl -fsSL https://opencode.ai/update/api/latest/cli/npm || true)
|
||||
metadata=$(curl -fsSL https://registry.npmjs.org/@opencode-ai%2fcli/beta || true)
|
||||
specific_version=$(echo "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
|
||||
package=$(echo "$metadata" | sed -n 's/.*"package":"\([^"]*\)".*/\1/p')
|
||||
|
||||
if [ -z "$specific_version" ] || [ -z "$package" ]; then
|
||||
if [ -z "$specific_version" ]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
exit 1
|
||||
fi
|
||||
package_scope="${package%/cli}"
|
||||
else
|
||||
# Strip leading 'v' if present
|
||||
requested_version="${requested_version#v}"
|
||||
specific_version=$requested_version
|
||||
fi
|
||||
|
||||
package_name="$package_scope/cli-$target"
|
||||
http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/$package_scope%2fcli-$target/$specific_version" || true)
|
||||
# Older clients install the minimum release before they can migrate package names.
|
||||
if [ "$http_status" = "404" ] && [ -n "$requested_version" ]; then
|
||||
package_name="@opencode-ai/cli-$target"
|
||||
http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/@opencode-ai%2fcli-$target/$specific_version" || true)
|
||||
fi
|
||||
package_name="@opencode-ai/cli-$target"
|
||||
http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/@opencode-ai%2fcli-$target/$specific_version" || true)
|
||||
if [ "$http_status" = "404" ]; then
|
||||
echo -e "${RED}Error: Version ${specific_version} is not available for $target${NC}"
|
||||
echo -e "${MUTED}Available versions: https://www.npmjs.com/package/$package_name?activeTab=versions${NC}"
|
||||
@@ -202,9 +193,9 @@ else
|
||||
|
||||
filename="cli-$target-$specific_version.tgz"
|
||||
url="https://registry.npmjs.org/$package_name/-/$filename"
|
||||
binary_name="$SOURCE_APP"
|
||||
binary_name="$APP"
|
||||
if [ "$os" = "windows" ]; then
|
||||
binary_name="$SOURCE_APP.exe"
|
||||
binary_name="$APP.exe"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -231,7 +222,12 @@ check_version() {
|
||||
installed_version="${installed_version##* }"
|
||||
installed_version="${installed_version#v}"
|
||||
|
||||
print_message info "${MUTED}Installed version: ${NC}$installed_version."
|
||||
if [[ "$installed_version" != "$specific_version" ]]; then
|
||||
print_message info "${MUTED}Installed version: ${NC}$installed_version."
|
||||
else
|
||||
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -336,46 +332,15 @@ download_and_install() {
|
||||
fi
|
||||
|
||||
tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
|
||||
local installed_binary="$APP"
|
||||
if [ "$os" = "windows" ]; then
|
||||
installed_binary="$APP.exe"
|
||||
fi
|
||||
mv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR/$installed_binary"
|
||||
chmod 755 "$INSTALL_DIR/$installed_binary"
|
||||
mv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR"
|
||||
chmod 755 "${INSTALL_DIR}/$binary_name"
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
|
||||
install_from_binary() {
|
||||
print_message info "\n${MUTED}Installing ${NC}$APP ${MUTED}from: ${NC}$binary_path"
|
||||
local installed_binary="$APP"
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*) installed_binary="$APP.exe" ;;
|
||||
esac
|
||||
cp "$binary_path" "$INSTALL_DIR/$installed_binary"
|
||||
chmod 755 "$INSTALL_DIR/$installed_binary"
|
||||
}
|
||||
|
||||
install_legacy_shim() {
|
||||
local shim_os="${os:-}"
|
||||
if [[ -z "$shim_os" ]]; then
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*) shim_os="windows" ;;
|
||||
esac
|
||||
fi
|
||||
rm -f "$INSTALL_DIR/opencode2" "$INSTALL_DIR/opencode2.exe" "$INSTALL_DIR/opencode2.cmd"
|
||||
if [[ "$shim_os" == "windows" ]]; then
|
||||
cat > "$INSTALL_DIR/opencode2.cmd" <<'EOF'
|
||||
@echo off
|
||||
"%~dp0opencode.exe" %*
|
||||
exit /b %errorlevel%
|
||||
EOF
|
||||
return
|
||||
fi
|
||||
cat > "$INSTALL_DIR/opencode2" <<'EOF'
|
||||
#!/bin/sh
|
||||
exec "$(dirname "$0")/opencode" "$@"
|
||||
EOF
|
||||
chmod 755 "$INSTALL_DIR/opencode2"
|
||||
cp "$binary_path" "${INSTALL_DIR}/$APP"
|
||||
chmod 755 "${INSTALL_DIR}/$APP"
|
||||
}
|
||||
|
||||
if [ -n "$binary_path" ]; then
|
||||
@@ -384,7 +349,6 @@ else
|
||||
check_version
|
||||
download_and_install
|
||||
fi
|
||||
install_legacy_shim
|
||||
|
||||
|
||||
add_to_path() {
|
||||
@@ -481,7 +445,7 @@ echo -e ""
|
||||
echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
|
||||
echo -e ""
|
||||
echo -e "cd <project> ${MUTED}# Open directory${NC}"
|
||||
echo -e "opencode ${MUTED}# Run command${NC}"
|
||||
echo -e "opencode2 ${MUTED}# Run command${NC}"
|
||||
echo -e ""
|
||||
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/v2/docs"
|
||||
echo -e ""
|
||||
|
||||
+10
-35
@@ -4,7 +4,7 @@
|
||||
bun,
|
||||
nodejs,
|
||||
darwin,
|
||||
callPackage,
|
||||
electron_41,
|
||||
makeWrapper,
|
||||
writableTmpDirAsHomeHook,
|
||||
autoPatchelfHook,
|
||||
@@ -13,12 +13,7 @@
|
||||
opencode,
|
||||
}:
|
||||
let
|
||||
electronPin =
|
||||
(lib.pipe ../packages/desktop/package.json [
|
||||
builtins.readFile
|
||||
builtins.fromJSON
|
||||
]).devDependencies.electron;
|
||||
electron = callPackage ./electron.nix { inherit electronPin; };
|
||||
electron = electron_41;
|
||||
in
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "opencode-desktop";
|
||||
@@ -40,8 +35,6 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
copyDesktopItems
|
||||
]
|
||||
++ lib.optionals stdenv.hostPlatform.isDarwin [
|
||||
darwin.cctools
|
||||
darwin.sigtool
|
||||
# Ad-hoc sign the .app: --config.mac.identity=null below skips signing.
|
||||
darwin.autoSignDarwinBinariesHook
|
||||
];
|
||||
@@ -55,7 +48,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
desktopName = "OpenCode";
|
||||
exec = "opencode-desktop %U";
|
||||
icon = "ai.opencode.desktop";
|
||||
# Electron derives X11 WM_CLASS from app.name.
|
||||
# Electron 41 derives X11 WM_CLASS from app.name.
|
||||
startupWMClass = "OpenCode";
|
||||
categories = [ "Development" ];
|
||||
});
|
||||
@@ -73,15 +66,16 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
''
|
||||
# https://github.com/electron/electron/issues/31121
|
||||
# mac builds use a .app bundle which doesnt have this issue
|
||||
+ lib.optionalString stdenv.hostPlatform.isLinux ''
|
||||
substituteInPlace \
|
||||
packages/desktop/src/main/windows/appearance.ts \
|
||||
packages/desktop/src/main/service/desktop-cli.ts \
|
||||
--replace-fail "process.resourcesPath" "'$out/opt/opencode-desktop/resources'"
|
||||
+ lib.optionalString stdenv.isLinux ''
|
||||
BASE_PATH=packages/desktop
|
||||
FILES=(src/main/windows.ts)
|
||||
for file in "''${FILES[@]}"; do
|
||||
substituteInPlace $BASE_PATH/$file \
|
||||
--replace-fail "process.resourcesPath" "'$out/opt/opencode-desktop/resources'"
|
||||
done
|
||||
'';
|
||||
|
||||
preBuild = ''
|
||||
echo "electron ${electron.version} from nixpkgs ${lib.version}, package.json pins ${electronPin}"
|
||||
cp -r "${electron.dist}" $HOME/.electron-dist
|
||||
chmod -R u+w $HOME/.electron-dist
|
||||
|
||||
@@ -95,18 +89,6 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
|
||||
cd packages/desktop
|
||||
|
||||
export OPENCODE_CLI_DIST="$TMPDIR/desktop-cli"
|
||||
cli_package=$(bun -e 'import { getCurrentCli } from "./scripts/utils.ts"; console.log(getCurrentCli().package.replace("@opencode/", ""))')
|
||||
# copyBuiltCliToResources joins this dist with the npm package name getCurrentCli()
|
||||
# reports, not the Nix build's name. It reads only .version from the manifest and
|
||||
# writes it as opencode-cli.version beside the binary.
|
||||
mkdir -p "$OPENCODE_CLI_DIST/$cli_package/bin"
|
||||
cp ${lib.getExe opencode} "$OPENCODE_CLI_DIST/$cli_package/bin/opencode"
|
||||
# OPENCODE_VERSION is what the bundled CLI prints for --version, so the manifest
|
||||
# and the executable cannot drift.
|
||||
bun -e 'await Bun.write(process.argv[1], JSON.stringify({ version: process.env.OPENCODE_VERSION }) + "\n")' \
|
||||
"$OPENCODE_CLI_DIST/$cli_package/package.json"
|
||||
|
||||
bun run build
|
||||
npx electron-builder --dir \
|
||||
--config electron-builder.config.ts \
|
||||
@@ -153,13 +135,6 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
"libc.musl-x86_64.so.1"
|
||||
];
|
||||
|
||||
passthru = {
|
||||
# electronVersion is what ships; electronPin is what packages/desktop/package.json
|
||||
# asks for. They differ whenever nixpkgs carries no release of the pinned minor.
|
||||
electronVersion = electron.version;
|
||||
inherit electronPin;
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "OpenCode Desktop App";
|
||||
mainProgram = "opencode-desktop";
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{ lib, pkgs, electronPin }:
|
||||
let
|
||||
# Nixpkgs owns the release hashes, so bumping the pin no longer means editing this repo. Only the
|
||||
# major is delegated, so what gets built trails the pin whenever nixpkgs has not shipped it yet.
|
||||
# That is safe: the bundle ships one native addon, node-pty's Node-API prebuild, and
|
||||
# only the win32 WSL runtime loads it, so nothing in the main process binds the
|
||||
# Electron ABI.
|
||||
major = lib.versions.major electronPin;
|
||||
in
|
||||
pkgs."electron_${major}-bin" or (throw "nixpkgs ${lib.version} carries no prebuilt electron ${major}: run `nix flake update nixpkgs`, or pin a major nixpkgs still carries")
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-Nrl3q4qV3YKGnSoKDjs0HvFyviVhtNfKEmMUIoREITc=",
|
||||
"aarch64-linux": "sha256-AfaHJIwoLA/9icnohKM2DL2dcmgSANfVT3oefOYBylE=",
|
||||
"aarch64-darwin": "sha256-nat5x4P4yGHqqBbw64CT0B66dmR3oSV3Qic1vMGnE4Q="
|
||||
"x86_64-linux": "sha256-NV1PD2fCgWEKsr9kR0pV9jgkC400dzoF7/DnI/fY5yI=",
|
||||
"aarch64-linux": "sha256-TDTdwE0mcHLrrKPDwPPBk3qIDl/PXJrLX6Zbwp7EH3I=",
|
||||
"aarch64-darwin": "sha256-6MEoiV1UKAWgC7C6PR4USCP/LLZXROfBfPg6sb2VVWg=",
|
||||
"x86_64-darwin": "sha256-8JV6YVZFq1BC++zpARxBWhQ+wuNJrWgTZJ6jfQhDybs="
|
||||
}
|
||||
}
|
||||
@@ -27,12 +27,11 @@ stdenvNoCC.mkDerivation {
|
||||
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
|
||||
lib.fileset.unions [
|
||||
../packages
|
||||
../services
|
||||
../bun.lock
|
||||
../package.json
|
||||
../patches
|
||||
../install # required by desktop build (cli.rs include_str!)
|
||||
../.github/TEAM_MEMBERS # required by @opencode/script
|
||||
../.github/TEAM_MEMBERS # required by @opencode-ai/script
|
||||
]
|
||||
);
|
||||
};
|
||||
@@ -81,5 +80,6 @@ stdenvNoCC.mkDerivation {
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
}
|
||||
+8
-34
@@ -8,11 +8,10 @@
|
||||
makeBinaryWrapper,
|
||||
models-dev,
|
||||
ripgrep,
|
||||
wayland,
|
||||
installShellFiles,
|
||||
versionCheckHook,
|
||||
writableTmpDirAsHomeHook,
|
||||
node_modules ? callPackage ./node_modules.nix { },
|
||||
node_modules ? callPackage ./node-modules.nix { },
|
||||
}:
|
||||
stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
pname = "opencode";
|
||||
@@ -63,10 +62,9 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
install -Dm755 dist/cli-*/bin/opencode $out/bin/opencode
|
||||
install -Dm755 dist/cli-*/bin/opencode2 $out/bin/opencode2
|
||||
|
||||
# OpenTUI dlopens Wayland for clipboard images.
|
||||
wrapProgram $out/bin/opencode \
|
||||
wrapProgram $out/bin/opencode2 \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath (
|
||||
[
|
||||
@@ -75,40 +73,16 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
# bun runs sysctl to detect if running on rosetta2
|
||||
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||
)
|
||||
} ${lib.optionalString stdenvNoCC.hostPlatform.isLinux ''
|
||||
--prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath [ wayland ]}
|
||||
''}
|
||||
|
||||
ln -s opencode $out/bin/opencode2
|
||||
}
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
||||
# v2 dropped the `completion` subcommand; --completions is the global flag.
|
||||
# --completions also accepts sh, which emits the same script as bash.
|
||||
# staged to files, substitute below rejects anything that is not a regular file
|
||||
$out/bin/opencode --completions bash > opencode.bash
|
||||
$out/bin/opencode --completions zsh > _opencode
|
||||
$out/bin/opencode --completions fish > opencode.fish
|
||||
|
||||
installShellCompletion --cmd opencode \
|
||||
--bash opencode.bash \
|
||||
--fish opencode.fish \
|
||||
--zsh _opencode
|
||||
|
||||
# OPENCODE_CLI_NAME is a build-time define, so the opencode2 copies are
|
||||
# renamed rather than regenerated. --replace-fail is a global literal
|
||||
# substitution, so any lowercase opencode that later appears in a
|
||||
# description or help text ships as opencode2 in the opencode2 copy.
|
||||
substitute opencode.bash opencode2.bash --replace-fail opencode opencode2
|
||||
substitute _opencode _opencode2 --replace-fail opencode opencode2
|
||||
substitute opencode.fish opencode2.fish --replace-fail opencode opencode2
|
||||
|
||||
# trick yargs into also generating zsh completions
|
||||
installShellCompletion --cmd opencode2 \
|
||||
--bash opencode2.bash \
|
||||
--fish opencode2.fish \
|
||||
--zsh _opencode2
|
||||
--bash <($out/bin/opencode2 completion) \
|
||||
--zsh <(SHELL=/bin/zsh $out/bin/opencode2 completion)
|
||||
'';
|
||||
|
||||
nativeInstallCheckInputs = [
|
||||
@@ -127,7 +101,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
description = "The open source coding agent";
|
||||
homepage = "https://opencode.ai";
|
||||
license = lib.licenses.mit;
|
||||
mainProgram = "opencode";
|
||||
mainProgram = "opencode2";
|
||||
inherit (node_modules.meta) platforms;
|
||||
};
|
||||
})
|
||||
+45
-54
@@ -2,35 +2,32 @@
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"version": "2.0.27",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.4.2",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/cli src/index.ts",
|
||||
"dev:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode service get password)\" exec bun run dev \"$@\" --server \"$(opencode service status)\"' --",
|
||||
"dev:vite": "bun run --cwd packages/cli --conditions=browser dev/vite.ts",
|
||||
"dev:vite:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode service get password)\" exec bun run dev:vite \"$@\" --server \"$(opencode service status)\"' --",
|
||||
"dev": "bun run --cwd packages/cli --conditions=browser src/index.ts",
|
||||
"dev:live": "OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode2 service get password)\" bun run dev --server \"$(opencode2 service status)\"",
|
||||
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||
"dev:www": "bun run --cwd services/www dev",
|
||||
"dev:www": "bun run --cwd packages/www dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"bench:devex": "bun run --cwd packages/app test:bench:devex",
|
||||
"lint": "oxlint && ast-grep scan -c script/ast-grep/gui-extensions/sgconfig.yml && bun script/sdk-docs.ts",
|
||||
"lint:changed": "bun script/lint-changed.ts",
|
||||
"lint": "oxlint",
|
||||
"lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/util/src packages/core/src packages/server/src packages/protocol/src packages/cli/src",
|
||||
"lint:effect-simplifications": "ast-grep scan -c script/ast-grep/effect-simplifications/sgconfig.yml --off=unused-suppression packages",
|
||||
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
|
||||
"test:effect-simplification-rules": "ast-grep test -c script/ast-grep/effect-simplifications/sgconfig.yml",
|
||||
"typecheck": "bun turbo typecheck --concurrency=3",
|
||||
"check": "bun run lint && bun run typecheck",
|
||||
"typecheck:profile": "bun script/profile-typecheck.ts",
|
||||
"typecheck:profile:packages": "bun script/profile-typecheck-packages.ts",
|
||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
||||
"prepare": "husky",
|
||||
"reserve-packages": "bun script/reserve-package-names.ts",
|
||||
"random": "echo 'Random script'",
|
||||
"sso": "aws sso login --sso-session=opencode --no-browser",
|
||||
"test": "echo 'do not run tests from root' && exit 1"
|
||||
@@ -38,29 +35,28 @@
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"services/*",
|
||||
"packages/console/*",
|
||||
"packages/stats/*"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.1",
|
||||
"@effect/platform-node": "4.0.1",
|
||||
"@effect/platform-node-shared": "4.0.1",
|
||||
"@effect/sql-sqlite-bun": "4.0.1",
|
||||
"@effect/opentelemetry": "4.0.0-rc.111",
|
||||
"@effect/platform-node": "4.0.0-rc.111",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.111",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.111",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.4.0",
|
||||
"@types/bun": "1.3.13",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.5.17",
|
||||
"@opentui/keymap": "0.5.17",
|
||||
"@opentui/solid": "0.5.17",
|
||||
"@opentui/core": "0.5.8",
|
||||
"@opentui/keymap": "0.5.8",
|
||||
"@opentui/solid": "0.5.8",
|
||||
"@tanstack/solid-virtual": "3.13.37",
|
||||
"@shikijs/stream": "4.4.3",
|
||||
"@shikijs/stream": "4.2.0",
|
||||
"@standard-schema/spec": "1.1.0",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.13",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@corvu/drawer": "0.2.4",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "24.12.2",
|
||||
@@ -68,19 +64,16 @@
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@pierre/diffs": "1.5.1",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.2.10",
|
||||
"opentui-spinner": "0.0.7",
|
||||
"@solid-primitives/event-listener": "2.4.6",
|
||||
"@solid-primitives/media": "2.3.6",
|
||||
"@solid-primitives/resize-observer": "2.2.0",
|
||||
"@solid-primitives/storage": "4.4.0",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.4",
|
||||
"dompurify": "3.4.14",
|
||||
"diff": "8.0.2",
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-rc.5-ab785fc",
|
||||
"drizzle-orm": "1.0.0-rc.5-169397b",
|
||||
"effect": "4.0.1",
|
||||
"fast-check": "4.9.0",
|
||||
"effect": "4.0.0-rc.111",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
@@ -88,27 +81,28 @@
|
||||
"fuzzysort": "3.1.0",
|
||||
"get-east-asian-width": "1.6.0",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "18.0.11",
|
||||
"marked": "18.0.7",
|
||||
"marked-shiki": "1.2.1",
|
||||
"remend": "1.3.1",
|
||||
"remend": "1.3.0",
|
||||
"@playwright/test": "1.59.1",
|
||||
"semver": "7.7.4",
|
||||
"typescript": "5.8.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
"resolve.exports": "2.0.3",
|
||||
"sst": "4.13.1",
|
||||
"shiki": "4.4.3",
|
||||
"shiki": "4.2.0",
|
||||
"solid-list": "0.3.0",
|
||||
"string-width": "7.2.0",
|
||||
"tailwindcss": "4.1.11",
|
||||
"vite": "7.3.6",
|
||||
"vite": "7.1.4",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "1.0.0",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
"@sentry/solid": "10.71.0",
|
||||
"@sentry/vite-plugin": "5.4.0",
|
||||
"solid-js": "1.9.15",
|
||||
"@sentry/solid": "10.36.0",
|
||||
"@sentry/vite-plugin": "4.6.0",
|
||||
"solid-js": "1.9.10",
|
||||
"solid-sonner": "0.3.1",
|
||||
"vite-plugin-solid": "2.11.10",
|
||||
"@lydell/node-pty": "1.2.0-beta.12"
|
||||
@@ -117,12 +111,10 @@
|
||||
"devDependencies": {
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@ast-grep/cli": "0.44.0",
|
||||
"@opencode/client": "workspace:*",
|
||||
"@oxlint/plugins": "1.60.0",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/react": "19.2.17",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"glob": "13.0.5",
|
||||
"husky": "9.1.7",
|
||||
@@ -135,8 +127,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/script": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"heap-snapshot-toolkit": "1.1.3",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
@@ -153,6 +145,10 @@
|
||||
"esbuild",
|
||||
"node-pty",
|
||||
"protobufjs",
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"tree-sitter-powershell",
|
||||
"web-tree-sitter",
|
||||
"electron"
|
||||
],
|
||||
"overrides": {
|
||||
@@ -162,10 +158,7 @@
|
||||
"@effect/platform-node-shared": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"bun-types": "1.4.2",
|
||||
"effect": "catalog:",
|
||||
"open": "11.0.4",
|
||||
"solid-js": "catalog:"
|
||||
"effect": "catalog:"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
|
||||
@@ -173,16 +166,14 @@
|
||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.15": "patches/solid-js@1.9.15.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
|
||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
|
||||
"@ff-labs/fff-bun@0.10.5": "patches/@ff-labs%2Ffff-bun@0.10.5.patch",
|
||||
"ghostty-web@github:anomalyco/ghostty-web#83c0a07": "patches/ghostty-web@0.3.0.patch",
|
||||
"vite@8.2.2": "patches/vite@8.2.2.patch",
|
||||
"@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch"
|
||||
"@ff-labs/fff-bun@0.10.5": "patches/@ff-labs%2Ffff-bun@0.10.5.patch"
|
||||
}
|
||||
}
|
||||
+281
-12
@@ -1,20 +1,289 @@
|
||||
# AI Package Guide
|
||||
|
||||
## Principles
|
||||
## Effect
|
||||
|
||||
- **Forward compatibility.** Providers ship new models, options, and values constantly. Accept unknown pass-through values (`Known | (string & {})` for string options); use closed literals only when OpenCode branches on the value.
|
||||
- **One canonical model.** The Schema classes in `src/schema/` are the data model. Do not add a parallel shape or a second construction path.
|
||||
- **Independent of Session.** Auth, permissions, plugins, and orchestration belong in Core. `LLM.stream`/`LLM.generate` run exactly one model call.
|
||||
- **Reuse protocols.** Providers compose existing protocols with `Route.make(...)` instead of cloning them.
|
||||
- Prefer `HttpClient.HttpClient` / `HttpClientResponse.HttpClientResponse` over web `fetch` / `Response` at package boundaries.
|
||||
- Use `Stream.Stream` for streaming data flow. Avoid ad hoc async generators or manual web reader loops unless an Effect `Stream` API cannot model the behavior.
|
||||
- Use Effect Schema codecs for JSON encode/decode (`Schema.fromJsonString(...)`) instead of direct `JSON.parse` / `JSON.stringify` in implementation code.
|
||||
- In `Effect.gen`, yield yieldable errors directly (`return yield* new MyError(...)`) instead of `Effect.fail(new MyError(...))`.
|
||||
- Use `Effect.void` instead of `Effect.succeed(undefined)` when the successful value is intentionally void.
|
||||
|
||||
## Code style
|
||||
## Conventions
|
||||
|
||||
- Use `HttpClient`, `Stream`, and Schema codecs instead of `fetch`, async generators, and `JSON.parse`/`JSON.stringify`.
|
||||
- `return yield* new MyError(...)` instead of `Effect.fail(...)`; `Effect.void` instead of `Effect.succeed(undefined)`.
|
||||
- Only `src/promise.ts` may use Promises.
|
||||
Per-type constructors live on the type, not as top-level re-exports. Use `Message.system(...)`, `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `LanguageModel.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, and `LLM.generateObject`. Use `LLMRequest.update(...)` when deriving canonical request data; do not add a duplicate `LLM.updateRequest(...)` path. Two ways to construct the same thing is one too many.
|
||||
|
||||
- Keep provider-defined string enums forward-compatible. Expose known values for autocomplete while accepting future values with `Known | (string & {})`; use `Schema.String` at runtime unless rejecting unknown values is required for correctness.
|
||||
|
||||
## Tests
|
||||
|
||||
- Use `testEffect(...)` from `test/lib/effect.ts` for tests that need Effect layers.
|
||||
- Provider tests replay cassettes: recorded HTTP exchanges stored as JSON in `test/fixtures/recordings/`, one file per scenario. Replay is the default and needs no API keys.
|
||||
- To record, unset `CI` and run with `RECORD=true` plus the provider's API key. Re-record only the cassette you are changing; recordings contain volatile IDs that create noisy diffs.
|
||||
- Use `testEffect(...)` from `test/lib/effect.ts` for tests requiring Effect layers.
|
||||
- Keep provider tests fixture-first. Live provider calls must stay behind `RECORD=true` and required API-key checks.
|
||||
|
||||
## Architecture
|
||||
|
||||
This package is an Effect Schema-first LLM core. The Schema classes in `src/schema/` are the canonical runtime data model. Convenience functions in `src/llm.ts` are thin constructors that return those same Schema class instances; they should improve callsites without creating a second model.
|
||||
|
||||
Session integration lives in `packages/core/src/session`: `runner/llm.ts` owns orchestration, `model-request.ts` lowers Session state into `LLMRequest`, and `model-transport.ts` selects transport behavior.
|
||||
|
||||
Keep this package independent of Session concerns. Session auth, permissions, plugins, telemetry headers, and runtime selection belong in Core.
|
||||
|
||||
### Request Flow
|
||||
|
||||
The intended callsite is:
|
||||
|
||||
```ts
|
||||
const request = LLM.request({
|
||||
model: OpenAI.configure({ apiKey }).responses("gpt-4o-mini"),
|
||||
system: "You are concise.",
|
||||
prompt: "Say hello.",
|
||||
})
|
||||
|
||||
const response = yield * LLMClient.generate(request)
|
||||
```
|
||||
|
||||
`LLM.request(...)` builds an `LLMRequest`. `LLMClient.generate(...)` reads the executable route carried by `request.model.route`, builds the provider-native body, asks the route's transport for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`.
|
||||
|
||||
Use `LLMClient.stream(request)` when callers want incremental `LLMEvent`s. Use `LLMClient.generate(request)` when callers want those same events collected into an `LLMResponse`.
|
||||
|
||||
Filter or narrow `LLMEvent` streams with `LLMEvent.is.*` (camelCase guards, e.g. `events.filter(LLMEvent.is.toolCall)`). The kebab-case `LLMEvent.guards["tool-call"]` form also works but prefer `is.*` in new code.
|
||||
|
||||
### Routes
|
||||
|
||||
A route is the runnable composition of four orthogonal pieces:
|
||||
|
||||
- **`Protocol`** (`src/route/protocol.ts`) — semantic API contract. Owns request body construction (`body.from`), the body schema (`body.schema`), the streaming-event schema (`stream.event`), and the event-to-`LLMEvent` state machine (`stream.step`). `Route.make(...)` validates and JSON-encodes the body from `body.schema` and decodes frames with `stream.event`. Examples: `OpenAIChat.protocol`, `OpenResponses.protocol`, `OpenAIResponses.protocol`, `AnthropicMessages.protocol`, `Gemini.protocol`, `BedrockConverse.protocol`.
|
||||
- **`Endpoint`** (`src/route/endpoint.ts`) — URL construction. The host, path, and route query live on the endpoint. `Endpoint.path("/chat/completions", { baseURL })` is the common case; pass a function for paths that embed the model id or a body field (e.g. `Endpoint.path(({ body }) => `/model/${body.modelId}/converse-stream`)`).
|
||||
- **`Auth`** (`src/route/auth.ts`) — per-request transport authentication. Provider facades configure credentials onto the route before model selection, usually via `Auth.bearer(apiKey)` or `Auth.header(name, apiKey)`. Routes that need per-request signing (Bedrock SigV4, future Vertex IAM, Azure AAD) implement `Auth` as a function that signs the body and merges signed headers into the result.
|
||||
- **`Framing`** (`src/route/framing.ts`) — bytes → frames. SSE (`Framing.sse`) is shared; Bedrock keeps its AWS event-stream framing as a typed `Framing<object>` value alongside its protocol.
|
||||
|
||||
Compose them via `Route.make(...)`:
|
||||
|
||||
```ts
|
||||
export const route = Route.make({
|
||||
id: "openai-chat",
|
||||
provider: "openai",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", {
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
}),
|
||||
auth: Auth.bearer(Auth.config("OPENAI_API_KEY")),
|
||||
framing: Framing.sse,
|
||||
})
|
||||
```
|
||||
|
||||
Route defaults are request-shaping defaults such as `headers`, `limits`, `generation`, `providerOptions`, and `http`. Endpoint host/query belongs on the route endpoint. Selected `LanguageModel` values carry only model id, provider id, and the configured route value. Model capability/catalog metadata lives outside this package; protocol support is enforced by request lowering and typed `AIError`s.
|
||||
|
||||
The four-axis decomposition is the reason DeepSeek, TogetherAI, Cerebras, Baseten, Fireworks, and DeepInfra all reuse `OpenAIChat.protocol` verbatim — each provider deployment is a 5-15 line `Route.make(...)` call instead of a 300-400 line route clone. Bug fixes in one protocol propagate to every consumer of that protocol in a single commit.
|
||||
|
||||
When a provider supports multiple physical transports, selection remains execution policy below its semantic route. `OpenResponsesChannel.transport(...)` owns the provider-neutral Responses WebSocket concept: it prepares one final request, executes HTTP by default, strips WebSocket-disallowed fields, and passes a generic channel exchange to a per-call `WebSocketChannelExecutor` when supplied. Provider-specific Responses routes opt in with handshake and connection-age policy. `Route.streamPrepared` owns decoding and acknowledges channel completion only after successful full consumption.
|
||||
|
||||
### URL Construction
|
||||
|
||||
`Endpoint` owns `{ baseURL, path, query }`. Each protocol route includes a canonical endpoint when the provider has one (e.g. `https://api.openai.com/v1`); provider helpers override endpoint fields by configuring the route before selecting a model. Generic OpenAI-compatible routes have no canonical URL and require configuration before execution.
|
||||
|
||||
For providers where the URL is derived from typed inputs (Azure resource name, Bedrock region), the provider helper configures the route endpoint before calling `.model(...)`. Use `AtLeastOne<T>` from `route/auth-options.ts` for inputs that accept either of two derivation paths (Azure: `resourceName` or `baseURL`).
|
||||
|
||||
### Provider Facades
|
||||
|
||||
Provider-facing APIs are configured facades over route values. Endpoint/auth/resource/API-version setup happens before model selection, and model selectors accept only a model or deployment id:
|
||||
|
||||
```ts
|
||||
const openai = OpenAI.configure({ apiKey, baseURL })
|
||||
const model = openai.responses("gpt-4o-mini")
|
||||
|
||||
const azure = Azure.configure({ resourceName, apiKey, apiVersion: "v1" })
|
||||
const deployment = azure.responses("my-deployment")
|
||||
|
||||
const gateway = CloudflareAIGateway.configure({ accountId, gatewayId, gatewayApiKey, apiKey })
|
||||
const proxied = gateway.model("openai/gpt-4o-mini")
|
||||
```
|
||||
|
||||
Keep provider facades small and explicit:
|
||||
|
||||
- Use branded `ProviderID.make(...)` and `ModelID.make(...)` where ids are constructed directly.
|
||||
- Use `model` for the default API path and named methods for provider-native alternatives such as OpenAI `responses` and `chat`.
|
||||
- Put provider-specific setup on `.configure(...)`; do not add `model(id, overrides)` as a duplicate construction path.
|
||||
- Export lower-level `routes` arrays separately only when advanced internal wiring needs them.
|
||||
- Prefer `apiKey` as provider-specific sugar and `auth` as the explicit override; keep them mutually exclusive in provider option types with `ProviderAuthOption`.
|
||||
- Resolve `apiKey` → `Auth` with `AuthOptions.bearer(options, "<PROVIDER>_API_KEY")` (it honors an explicit `auth` override and falls back to `Auth.config(envVar)` so missing keys surface a typed `Authentication` error rather than a runtime crash).
|
||||
- Use separate top-level facades for products with different required setup, such as `CloudflareAIGateway` and `CloudflareWorkersAI`.
|
||||
|
||||
`Provider.make(...)` remains available for simple static provider definitions, but new built-in providers should prefer plain configured facades unless a helper removes real duplication without adding runtime behavior.
|
||||
|
||||
### Provider Package Entrypoints
|
||||
|
||||
Catalog-selected native providers use package-like export paths from `@opencode-ai/ai`. They are internal entrypoints in one npm package, not separately published provider packages. Every entrypoint implements `ProviderPackage.Definition` and exposes `model(modelID, settings)`, where settings are serializable provider configuration plus common `headers`, `body`, and `limits` overlays.
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/openai/responses"
|
||||
|
||||
const selected = model("gpt-5", {
|
||||
apiKey,
|
||||
})
|
||||
```
|
||||
|
||||
Keep semantic APIs as separate entrypoints, such as OpenAI `chat` and `responses`. Transport is execution policy: OpenAI Responses uses HTTP by default and may receive a per-call WebSocket channel executor through `StreamOptions` without changing model or route identity.
|
||||
|
||||
Do not expose `Route` in provider package settings. Route composition stays an implementation detail behind `model(...)`.
|
||||
|
||||
The dependency arrow points down: `providers/*.ts` files import protocol routes and auth-option utilities; protocol modules import `endpoint`, `auth`, `framing`, and transport pieces. Protocols do not import provider facades. Lower-level modules know nothing about provider catalog metadata. `OpenAIResponses` composes the provider-neutral `OpenResponses` protocol; the baseline never imports the OpenAI extension.
|
||||
|
||||
### Shared protocol helpers
|
||||
|
||||
`ProviderShared` exports a small toolkit used inside protocol implementations to keep them focused on provider-native shapes:
|
||||
|
||||
- `joinText(parts)` — joins an array of `TextPart` (or anything with a `.text`) with newlines. Use this anywhere a protocol flattens text content into a single string for a provider field.
|
||||
- `parseToolInput(route, name, raw)` — Schema-decodes a tool-call argument string with the canonical "Invalid JSON input for `<route>` tool call `<name>`" error message. Treats empty input as `{}`.
|
||||
- `parseJson(route, raw, message)` — generic JSON-via-Schema decode for non-tool bodies.
|
||||
- `eventError(route, message, ...)` — typed `InvalidProviderOutput` constructor for stream-time decode failures.
|
||||
- `validateWith(decoder)` — maps Schema decode errors to `InvalidRequest`. `Route.make(...)` uses this for body validation; lower-level routes can reuse it.
|
||||
- `matchToolChoice(provider, choice, branches)` — branches over `LLMRequest["toolChoice"]` for provider-specific lowering.
|
||||
|
||||
If you find yourself copying a 3-to-5-line snippet between two protocols, lift it into `ProviderShared` next to these helpers rather than duplicating.
|
||||
|
||||
### Chronological System Updates
|
||||
|
||||
`LLMRequest.system` is the initial privileged prompt that applies ahead of the conversation. `Message.system(...)` is a separate, provider-neutral chronological operator update inside `LLMRequest.messages`; it applies only from its position in history onward and accepts text content only.
|
||||
|
||||
Native chronological system messages are route/model-specific. Open Responses lowers them to standard `developer` messages, while Anthropic Messages lowers them to native system messages for Claude Opus 4.8 (`claude-opus-4-8`). Other routes and models intentionally lower the update in place into ordinary user-compatible text using this stable escaped representation:
|
||||
|
||||
```text
|
||||
<system-update>
|
||||
...
|
||||
</system-update>
|
||||
```
|
||||
|
||||
The wrapped-user fallback preserves ordering while visibly lowering authority. Never silently pass a raw chronological `role: "system"` through a route that might reject it. Do not insert raw retrieved documents, tool output, or web content into privileged chronological system updates; keep untrusted content in ordinary user/tool channels.
|
||||
|
||||
### Tools
|
||||
|
||||
Tool loops are represented in common messages and events:
|
||||
|
||||
```ts
|
||||
const call = ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })
|
||||
const result = Message.tool({ id: "call_1", name: "lookup", result: { forecast: "sunny" } })
|
||||
|
||||
const followUp = LLM.request({
|
||||
model,
|
||||
messages: [Message.user("Weather?"), Message.assistant([call]), result],
|
||||
})
|
||||
```
|
||||
|
||||
Routes lower these into provider-native assistant tool-call messages and tool-result messages. Streaming providers should emit `tool-input-delta` events while arguments arrive, then a final `tool-call` event with parsed input.
|
||||
|
||||
### Tool dispatch
|
||||
|
||||
`LLM.stream(request)` and `LLM.generate(request)` each run exactly one model call. Add tool schemas to `request.tools` with `Tool.toDefinitions(tools)`. When a caller wants the package's typed one-call execution behavior, pass each canonical local `tool-call` event to `ToolRuntime.dispatch(tools, call)`.
|
||||
|
||||
```ts
|
||||
const get_weather = Tool.make({
|
||||
description: "Get current weather for a city",
|
||||
parameters: Schema.Struct({ city: Schema.String }),
|
||||
success: Schema.Struct({ temperature: Schema.Number, condition: Schema.String }),
|
||||
execute: (input) =>
|
||||
Effect.gen(function* () {
|
||||
const data = yield* WeatherApi.fetch(input.city)
|
||||
return { temperature: data.temp, condition: data.cond }
|
||||
}),
|
||||
})
|
||||
|
||||
const tools = { get_weather, get_time, ... }
|
||||
const events = yield* LLM.stream(
|
||||
LLMRequest.update(request, { tools: Tool.toDefinitions(tools) }),
|
||||
).pipe(Stream.runCollect)
|
||||
|
||||
const call = Array.from(events).find(LLMEvent.is.toolCall)
|
||||
if (call && !call.providerExecuted) {
|
||||
const dispatched = yield* ToolRuntime.dispatch(tools, call)
|
||||
// Persist call + dispatched.result, then construct the next request explicitly.
|
||||
}
|
||||
```
|
||||
|
||||
The dispatcher:
|
||||
|
||||
- On `tool-call`: looks up the named tool, decodes input against `parameters` Schema, dispatches to the typed `execute`, encodes the result against `success` Schema, and returns canonical `tool-result` events.
|
||||
- Does not stream providers, construct Session events, schedule fibers, append history, count steps, or continue model rounds.
|
||||
- Leaves persistence and continuation to the enclosing product flow.
|
||||
|
||||
Handler dependencies (services, permissions, plugin hooks, abort handling) are closed over by the consumer at tool-construction time. Build the tools record inside an `Effect.gen` once and reuse it across many dispatches.
|
||||
|
||||
Errors must be expressed as `ToolFailure`. The runtime catches it and emits a `tool-error` event, then a `tool-result` of `type: "error"`, so the model can self-correct on the next step. Anything that is not a `ToolFailure` is treated as a defect and fails the stream. Three recoverable error paths produce `tool-error` events:
|
||||
|
||||
- The model called an unknown tool name.
|
||||
- Input failed the `parameters` Schema.
|
||||
- The handler returned a `ToolFailure`.
|
||||
|
||||
Provider-defined / hosted tools (Anthropic `web_search` / `code_execution` / `web_fetch`, OpenAI Responses `web_search_call` / `file_search_call` / `code_interpreter_call` / `mcp_call` / `image_generation_call` / `computer_use_call`) pass through the runtime untouched:
|
||||
|
||||
- Routes surface the model's call as a `tool-call` event with `providerExecuted: true`, and the provider's result as a matching `tool-result` event with `providerExecuted: true`.
|
||||
- Callers detect `providerExecuted` on `tool-call` and **skip local dispatch** — no handler is invoked and no `tool-error` is raised for "unknown tool". The provider already executed it.
|
||||
- Callers that continue should retain both events in explicit history when the protocol requires it. Anthropic encodes them back as `server_tool_use` + `web_search_tool_result` (or `code_execution_tool_result` / `web_fetch_tool_result`) blocks; OpenAI Responses callers typically use `previous_response_id` instead of resending hosted-tool items.
|
||||
|
||||
Add provider-defined tools to `request.tools` (no runtime entry needed). The matching route must know how to lower the tool definition into the provider-native shape; right now Anthropic accepts `web_search` / `code_execution` / `web_fetch` and OpenAI Responses accepts the hosted tool names listed above.
|
||||
|
||||
## Protocol File Style
|
||||
|
||||
Protocol files should look self-similar. Provider quirks belong behind named helpers so a new route can be reviewed by comparing the same sections across files.
|
||||
|
||||
### Section order
|
||||
|
||||
Use this order for every protocol module:
|
||||
|
||||
1. Public model input
|
||||
2. Request body schema
|
||||
3. Streaming event schema
|
||||
4. Parser state
|
||||
5. Request body construction (`fromRequest`)
|
||||
6. Stream parsing (`step` and per-event handlers)
|
||||
7. Protocol and route
|
||||
8. Protocol route export
|
||||
|
||||
### Rules
|
||||
|
||||
- Keep protocol files focused on the protocol. Move provider-specific projection, signing, media normalization, or other bulky transformations into `src/protocols/utils/*`.
|
||||
- Use `Effect.fn("Provider.fromRequest")` for request body construction entrypoints. Use `Effect.fn(...)` for event handlers that yield effects; keep purely synchronous handlers as plain functions returning a `StepResult` that the dispatcher lifts via `Effect.succeed(...)`.
|
||||
- Parser state owns terminal information. The state machine records finish reason, usage, and pending tool calls; emit one terminal `finish` event (or `provider-error`) for each completed response. If a provider splits reason and usage across events, merge them in parser state before flushing.
|
||||
- Emit exactly one terminal `finish` event for a completed response, normally after a matching `step-finish`. Use `stream.terminal` to stop reading when the provider has a completion sentinel; use `stream.onHalt` when the final event must be flushed after the framed stream ends.
|
||||
- Use shared helpers for repeated protocol policy such as text joining, usage totals, JSON parsing, and tool-call accumulation. `ToolStream` (`protocols/utils/tool-stream.ts`) accumulates streamed tool-call arguments uniformly.
|
||||
- Make intentional provider differences explicit in helper names or comments. If two protocol files differ visually, the reason should be obvious from the names.
|
||||
- Prefer dispatched per-event handlers (`onMessageStart`, `onContentBlockDelta`, ...) called from a small top-level `step` switch over a long if-chain. The dispatcher keeps the event surface visible at a glance.
|
||||
- Keep tests in the same conceptual order as the protocol: basic prepare, tools prepare, unsupported lowering, text/usage parsing, tool streaming, finish reasons, provider errors.
|
||||
|
||||
### Review checklist
|
||||
|
||||
- Can the file be skimmed side-by-side with `openai-chat.ts` without hunting for equivalent sections?
|
||||
- Are provider quirks named, isolated, and covered by focused tests?
|
||||
- Does request body construction validate unsupported common content at the protocol boundary?
|
||||
- Does stream parsing emit stable common events without leaking provider event order to callers?
|
||||
- Does `toolChoice: "none"` behavior read as intentional?
|
||||
|
||||
## Recording Tests
|
||||
|
||||
Recorded tests use one cassette file per scenario. A cassette holds an ordered array of `{ request, response }` interactions, so multi-step flows (tool loops, retries, polling) record into a single file. Use `recordedTests({ prefix, requires })` and let the helper derive cassette names from test names:
|
||||
|
||||
```ts
|
||||
const recorded = recordedTests({ prefix: "openai-chat", requires: ["OPENAI_API_KEY"] })
|
||||
|
||||
recorded.effect("streams text", () =>
|
||||
Effect.gen(function* () {
|
||||
// test body
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Replay is the default. `RECORD=true` records fresh cassettes locally and requires the listed env vars; unset `CI` before recording because CI always forces replay. Cassettes are written as pretty-printed JSON so multi-interaction diffs stay reviewable.
|
||||
|
||||
Pass `provider`, `protocol`, and optional `tags` to `recordedTests(...)` / `recorded.effect.with(...)` so cassettes carry searchable metadata. Use recorded-test filters to replay or record a narrow subset without rewriting a whole file:
|
||||
|
||||
- `RECORDED_PROVIDER=openai` matches tests tagged with `provider:openai`; comma-separated values are allowed.
|
||||
- `RECORDED_PREFIX=openai-chat` matches cassette groups by `recordedTests({ prefix })`; comma-separated values are allowed.
|
||||
- `RECORDED_TAGS=tool` requires all listed tags to be present, e.g. `RECORDED_TAGS=provider:togetherai,tool`.
|
||||
- `RECORDED_TEST="streams text"` matches by test name, kebab-case test id, or cassette path.
|
||||
|
||||
Filters apply in replay and record mode. Combine them with `RECORD=true` when refreshing only one provider or scenario.
|
||||
|
||||
**Binary response bodies.** Most providers stream text (SSE, JSON). The recorder treats known textual media types (`text/*`, JSON/XML structured types, JavaScript, forms, YAML, and SVG) as text and stores every other response as base64 with `bodyEncoding: "base64"`. This preserves binary formats such as AWS event-stream frames without a lossy UTF-8 round trip.
|
||||
|
||||
**Matching strategy.** A runtime request atomically claims the first unused recorded interaction that matches its method, URL, allow-listed headers, and canonical JSON body. Distinct requests may replay in any order or concurrently. Repeated identical requests consume their matching responses in cassette order, preserving deterministic retry and polling behavior. `scriptedResponses` (in `test/lib/http.ts`) is the deterministic counterpart for tests that don't need a live provider; it scripts response bodies in order without reading from disk.
|
||||
|
||||
Do not blanket re-record an entire test file when adding one cassette. `RECORD=true` rewrites every recorded case that runs, and provider streams contain volatile IDs, timestamps, fingerprints, and obfuscation fields. Prefer deleting the one cassette you intend to refresh, or run a focused test pattern that only registers the scenario you want to record. Keep stable existing cassettes unchanged unless their request shape or expected behavior changed.
|
||||
+135
-1134
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,7 @@
|
||||
import { Config, Effect, Formatter, Schema, Stream } from "effect"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { AIClient, Image, LLM, LLMRequest, Media, Message, ProviderID, Tool, ToolRuntime } from "@opencode/ai"
|
||||
import { Route, Auth, Endpoint, Framing, Protocol } from "@opencode/ai/route"
|
||||
import { OpenAI } from "@opencode/ai/providers"
|
||||
import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect"
|
||||
import { LLM, LLMClient, LLMRequest, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/ai"
|
||||
import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor } from "@opencode-ai/ai/route"
|
||||
import { OpenAI } from "@opencode-ai/ai/providers"
|
||||
|
||||
/**
|
||||
* A runnable walkthrough of the LLM package use-site API.
|
||||
@@ -15,20 +14,17 @@ import { OpenAI } from "@opencode/ai/providers"
|
||||
* hover imports and local values to see how the public API is typed.
|
||||
*/
|
||||
|
||||
const apiKey = Config.Redacted("OPENAI_API_KEY")
|
||||
const apiKey = Config.redacted("OPENAI_API_KEY")
|
||||
|
||||
// 1. Configure a provider. The configured facade records provider identity,
|
||||
// deployment options, authentication, and defaults. Per-modality selectors pick
|
||||
// the API: `.responses(...)` / `.chat(...)` for LLM calls and `.image(...)` for
|
||||
// image generation.
|
||||
const openai = OpenAI.configure({
|
||||
// 1. Pick a model. The provider helper records provider identity, protocol
|
||||
// choice, capabilities, deployment options, authentication, and defaults.
|
||||
const model = OpenAI.configure({
|
||||
apiKey,
|
||||
generation: { maxTokens: 160 },
|
||||
providerOptions: {
|
||||
store: false,
|
||||
},
|
||||
})
|
||||
const model = openai.responses("gpt-4o-mini")
|
||||
}).model("gpt-4o-mini")
|
||||
|
||||
// 2. Build a provider-neutral request. This is useful when reusing one request
|
||||
// across generate and stream examples.
|
||||
@@ -213,37 +209,18 @@ const FakeEcho = {
|
||||
}),
|
||||
}
|
||||
|
||||
// 8. Image generation uses the same facade and the same request/generate shape.
|
||||
// `response.image` is a `Media.Asset`: bytes decode lazily and are cached, and
|
||||
// `Media.write` persists them through the Effect `FileSystem`.
|
||||
const generateImage = Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
model: openai.image("gpt-image-1-mini"),
|
||||
prompt: "A flat black circle centered on a plain white background.",
|
||||
size: "1024x1024",
|
||||
format: "jpeg",
|
||||
providerOptions: { quality: "low" },
|
||||
})
|
||||
|
||||
console.log("\n== image ==")
|
||||
console.log("media type:", response.image.mediaType)
|
||||
console.log("bytes:", (yield* response.image.bytes()).byteLength)
|
||||
console.log("usage", Formatter.formatJson(response.usage, { space: 2 }))
|
||||
yield* Media.write(response.image, "tutorial-image.jpg").pipe(Effect.provide(NodeFileSystem.layer))
|
||||
})
|
||||
|
||||
// Provide every modality client and the HTTP request executor once with
|
||||
// `AIClient.layer` (`AIClient.layerWith(executor)` swaps the executor). Keep one
|
||||
// path enabled at a time so the tutorial can demonstrate generate, stream, or
|
||||
// Provide the LLM runtime and the HTTP request executor once. Keep one path
|
||||
// enabled at a time so the tutorial can demonstrate generate, stream, or
|
||||
// tool-loop behavior without spending tokens on every example.
|
||||
const requestExecutorLayer = RequestExecutor.fetchLayer
|
||||
const llmClientLayer = LLMClient.layer.pipe(Layer.provide(requestExecutorLayer))
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
// yield* generateOnce
|
||||
// yield* streamText
|
||||
// yield* generateStructuredObject
|
||||
// yield* generateDynamicObject.pipe(Effect.andThen((response) => Effect.sync(() => console.log(response.object))))
|
||||
// yield* generateImage
|
||||
yield* streamWithTools
|
||||
}).pipe(Effect.provide(AIClient.layer))
|
||||
}).pipe(Effect.provide(Layer.mergeAll(requestExecutorLayer, llmClientLayer)))
|
||||
|
||||
Effect.runPromise(program)
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.27",
|
||||
"name": "@opencode/ai",
|
||||
"version": "1.17.20",
|
||||
"name": "@opencode-ai/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@@ -15,24 +15,22 @@
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./promise": "./src/promise.ts",
|
||||
"./testing": "./src/testing.ts",
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode/http-recorder": "workspace:*",
|
||||
"@opencode-ai/http-recorder": "workspace:*",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
"@opencode/schema": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"aws4fetch": "1.0.20",
|
||||
"effect": "catalog:",
|
||||
"google-auth-library": "10.5.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bun
|
||||
import { Script } from "@opencode/script"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import { $ } from "bun"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@ import * as path from "node:path"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { AwsV4Signer } from "aws4fetch"
|
||||
import { Config, ConfigProvider, Effect, FileSystem, PlatformError, Redacted } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/http"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/unstable/http"
|
||||
import * as ProviderShared from "../src/protocols/shared"
|
||||
import { CloudflareAIGateway } from "../src/providers/cloudflare-ai-gateway.js"
|
||||
import { CloudflareWorkersAI } from "../src/providers/cloudflare-workers-ai.js"
|
||||
import * as Cloudflare from "../src/providers/cloudflare"
|
||||
|
||||
type Provider = {
|
||||
readonly id: string
|
||||
@@ -104,111 +103,6 @@ const PROVIDERS: ReadonlyArray<Provider> = [
|
||||
vars: [{ name: "XAI_API_KEY" }],
|
||||
validate: (env) => validateBearer("https://api.x.ai/v1/models", Redacted.make(env.XAI_API_KEY)),
|
||||
},
|
||||
{
|
||||
id: "fal",
|
||||
label: "fal",
|
||||
tier: "canary",
|
||||
note: "fal queue image and video recorded tests",
|
||||
vars: [{ name: "FAL_KEY" }],
|
||||
// fal has no free authenticated list endpoint; a 404 for an unknown request id proves the key was accepted.
|
||||
validate: (env) =>
|
||||
Effect.gen(function* () {
|
||||
const http = yield* HttpClient.HttpClient
|
||||
const response = yield* http.execute(
|
||||
HttpClientRequest.get(
|
||||
"https://queue.fal.run/fal-ai/veo3.1/requests/00000000-0000-0000-0000-000000000000/status",
|
||||
).pipe(HttpClientRequest.setHeaders({ authorization: `Key ${Redacted.value(Redacted.make(env.FAL_KEY))}` })),
|
||||
)
|
||||
if (response.status === 404) return undefined
|
||||
return yield* responseError(response)
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "black-forest-labs",
|
||||
label: "Black Forest Labs",
|
||||
tier: "canary",
|
||||
note: "BFL FLUX queued image recorded tests",
|
||||
vars: [{ name: "BFL_API_KEY" }],
|
||||
validate: (env) =>
|
||||
HttpClientRequest.get("https://api.bfl.ai/v1/credits").pipe(
|
||||
HttpClientRequest.setHeader("x-key", Redacted.value(Redacted.make(env.BFL_API_KEY))),
|
||||
executeRequest,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "replicate",
|
||||
label: "Replicate",
|
||||
tier: "canary",
|
||||
note: "Replicate prediction image recorded tests",
|
||||
vars: [{ name: "REPLICATE_API_TOKEN" }],
|
||||
validate: (env) => validateBearer("https://api.replicate.com/v1/account", Redacted.make(env.REPLICATE_API_TOKEN)),
|
||||
},
|
||||
{
|
||||
id: "stability",
|
||||
label: "Stability AI",
|
||||
tier: "canary",
|
||||
note: "Stability inline generate and queued upscale recorded tests",
|
||||
vars: [{ name: "STABILITY_API_KEY" }],
|
||||
validate: (env) => validateBearer("https://api.stability.ai/v1/user/balance", Redacted.make(env.STABILITY_API_KEY)),
|
||||
},
|
||||
{
|
||||
id: "runway",
|
||||
label: "Runway",
|
||||
tier: "canary",
|
||||
note: "Runway task video recorded tests",
|
||||
vars: [{ name: "RUNWAYML_API_SECRET" }],
|
||||
validate: (env) =>
|
||||
validateBearer("https://api.dev.runwayml.com/v1/organization", Redacted.make(env.RUNWAYML_API_SECRET), {
|
||||
"X-Runway-Version": "2024-11-06",
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "elevenlabs",
|
||||
label: "ElevenLabs",
|
||||
tier: "canary",
|
||||
note: "ElevenLabs text-to-speech recorded tests",
|
||||
vars: [{ name: "ELEVENLABS_API_KEY" }],
|
||||
validate: (env) =>
|
||||
HttpClientRequest.get("https://api.elevenlabs.io/v1/models").pipe(
|
||||
HttpClientRequest.setHeader("xi-api-key", Redacted.value(Redacted.make(env.ELEVENLABS_API_KEY))),
|
||||
executeRequest,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "cartesia",
|
||||
label: "Cartesia",
|
||||
tier: "canary",
|
||||
note: "Cartesia text-to-speech recorded tests",
|
||||
vars: [{ name: "CARTESIA_API_KEY" }],
|
||||
validate: (env) =>
|
||||
validateBearer("https://api.cartesia.ai/voices?limit=1", Redacted.make(env.CARTESIA_API_KEY), {
|
||||
"Cartesia-Version": "2026-08-14",
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "deepgram",
|
||||
label: "Deepgram",
|
||||
tier: "canary",
|
||||
note: "Deepgram Aura text-to-speech and Nova transcription recorded tests",
|
||||
vars: [{ name: "DEEPGRAM_API_KEY" }],
|
||||
validate: (env) =>
|
||||
HttpClientRequest.get("https://api.deepgram.com/v1/projects").pipe(
|
||||
HttpClientRequest.setHeader("authorization", `Token ${Redacted.value(Redacted.make(env.DEEPGRAM_API_KEY))}`),
|
||||
executeRequest,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "assemblyai",
|
||||
label: "AssemblyAI",
|
||||
tier: "canary",
|
||||
note: "AssemblyAI queued transcription recorded tests",
|
||||
vars: [{ name: "ASSEMBLYAI_API_KEY" }],
|
||||
validate: (env) =>
|
||||
HttpClientRequest.get("https://api.assemblyai.com/v2/transcript?limit=1").pipe(
|
||||
HttpClientRequest.setHeader("authorization", Redacted.value(Redacted.make(env.ASSEMBLYAI_API_KEY))),
|
||||
executeRequest,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "cloudflare-ai-gateway",
|
||||
label: "Cloudflare AI Gateway",
|
||||
@@ -226,11 +120,11 @@ const PROVIDERS: ReadonlyArray<Provider> = [
|
||||
],
|
||||
validate: (env) =>
|
||||
validateChat({
|
||||
url: `${CloudflareAIGateway.baseURL({
|
||||
url: `${Cloudflare.aiGatewayBaseURL({
|
||||
accountId: env.CLOUDFLARE_ACCOUNT_ID,
|
||||
gatewayId: env.CLOUDFLARE_GATEWAY_ID || undefined,
|
||||
})}/chat/completions`,
|
||||
token: Redacted.make(envValue(env, CloudflareAIGateway.authEnvVars)),
|
||||
token: Redacted.make(envValue(env, Cloudflare.aiGatewayAuthEnvVars)),
|
||||
tokenHeader: "cf-aig-authorization",
|
||||
model: "workers-ai/@cf/meta/llama-3.1-8b-instruct",
|
||||
}),
|
||||
@@ -246,8 +140,8 @@ const PROVIDERS: ReadonlyArray<Provider> = [
|
||||
],
|
||||
validate: (env) =>
|
||||
validateChat({
|
||||
url: `${CloudflareWorkersAI.baseURL({ accountId: env.CLOUDFLARE_ACCOUNT_ID })}/chat/completions`,
|
||||
token: Redacted.make(envValue(env, CloudflareWorkersAI.authEnvVars)),
|
||||
url: `${Cloudflare.workersAIBaseURL({ accountId: env.CLOUDFLARE_ACCOUNT_ID })}/chat/completions`,
|
||||
token: Redacted.make(envValue(env, Cloudflare.workersAIAuthEnvVars)),
|
||||
model: "@cf/meta/llama-3.1-8b-instruct",
|
||||
}),
|
||||
},
|
||||
@@ -383,7 +277,7 @@ const readEnvFile = Effect.fn("RecordingEnv.readFile")(function* () {
|
||||
})
|
||||
|
||||
const readConfigString = (provider: ConfigProvider.ConfigProvider, name: string) =>
|
||||
Config.String(name)
|
||||
Config.string(name)
|
||||
.parse(provider)
|
||||
.pipe(
|
||||
Effect.match({
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Layer } from "effect"
|
||||
import { ImageClient } from "./image-client.js"
|
||||
import { LLMClient } from "./route/client.js"
|
||||
import { RequestExecutor } from "./route/executor.js"
|
||||
import { SpeechClient } from "./speech-client.js"
|
||||
import { TranscriptionClient } from "./transcription-client.js"
|
||||
import { VideoClient } from "./video-client.js"
|
||||
|
||||
/** Every modality client over `executor`, which stays in the output so `asset.bytes()` and `Media.write` resolve. */
|
||||
export const layerWith = <E, R>(executor: Layer.Layer<RequestExecutor.Service, E, R>) =>
|
||||
Layer.mergeAll(
|
||||
LLMClient.layer,
|
||||
ImageClient.layer,
|
||||
VideoClient.layer,
|
||||
SpeechClient.layer,
|
||||
TranscriptionClient.layer,
|
||||
).pipe(Layer.provideMerge(executor))
|
||||
|
||||
/** Every modality client plus the executor over `RequestExecutor.fetchLayer`: the one layer most programs need. */
|
||||
export const layer = layerWith(RequestExecutor.fetchLayer)
|
||||
|
||||
export type Services = Layer.Success<typeof layer>
|
||||
|
||||
export * as AIClient from "./ai-client.js"
|
||||
@@ -11,8 +11,7 @@
|
||||
// Manual `cache: CacheHint` placements on individual parts are preserved and
|
||||
// count against the four-breakpoint budget; auto only fills remaining slots.
|
||||
import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options.js"
|
||||
import { LLMRequest, Message, ToolDefinition, type ContentPart, type ToolEntry } from "./schema/messages.js"
|
||||
import { effortUpdate } from "./effort-updates.js"
|
||||
import { LLMRequest, Message, ToolDefinition, type ContentPart } from "./schema/messages.js"
|
||||
|
||||
const AUTO: CachePolicyObject = {
|
||||
tools: true,
|
||||
@@ -38,35 +37,12 @@ const resolve = (policy: CachePolicy | undefined): CachePolicyObject => {
|
||||
// prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the
|
||||
// whole policy pass for these — emitting hints would be harmless but pointless.
|
||||
const RESPECTS_INLINE_HINTS = new Set([
|
||||
"alibaba-chat",
|
||||
"alibaba-messages",
|
||||
"anthropic-messages",
|
||||
"anthropic-compatible-messages",
|
||||
"bedrock-mantle-messages",
|
||||
"cloudflare-ai-gateway-messages",
|
||||
"google-vertex-messages",
|
||||
"meta-messages",
|
||||
"minimax-messages",
|
||||
"moonshot-messages",
|
||||
"vercel-ai-gateway-messages",
|
||||
"zai-coding-messages",
|
||||
"bedrock-converse",
|
||||
"openrouter",
|
||||
"digitalocean",
|
||||
])
|
||||
|
||||
// OpenRouter and Vercel AI Gateway upstreams other than Anthropic and Alibaba Qwen cache without breakpoints.
|
||||
// Gemini uses only the last breakpoint, so a conversation-tail breakpoint writes a new cache every step and costs
|
||||
// more than none. Qwen ignores breakpoints on tool definitions and caches tools with the system prompt.
|
||||
const QWEN: CachePolicyObject = { system: true, messages: { tail: 1 } }
|
||||
const gatewayPolicy = (modelID: string): CachePolicyObject => {
|
||||
// `~anthropic/claude-sonnet-latest` style IDs are OpenRouter aliases for the latest model in a family.
|
||||
const id = modelID.replace(/^~/, "")
|
||||
if (id.startsWith("anthropic/")) return AUTO
|
||||
if (id.startsWith("qwen/") || id.startsWith("alibaba/qwen")) return QWEN
|
||||
return NONE
|
||||
}
|
||||
|
||||
const makeHint = (ttlSeconds: number | undefined): CacheHint =>
|
||||
ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" })
|
||||
|
||||
@@ -74,24 +50,18 @@ interface Budget {
|
||||
remaining: number
|
||||
}
|
||||
|
||||
const markLastTool = (tools: ReadonlyArray<ToolEntry>, hint: CacheHint, budget: Budget): ReadonlyArray<ToolEntry> => {
|
||||
const target = tools.at(-1)
|
||||
if (target === undefined) return tools
|
||||
if (target.type === "namespace") {
|
||||
const nested = markLastTool(target.tools, hint, budget)
|
||||
return nested === target.tools ? tools : [...tools.slice(0, -1), { ...target, tools: nested }]
|
||||
}
|
||||
if (target.cache || budget.remaining === 0) return tools
|
||||
const markLastTool = (
|
||||
tools: ReadonlyArray<ToolDefinition>,
|
||||
hint: CacheHint,
|
||||
budget: Budget,
|
||||
): ReadonlyArray<ToolDefinition> => {
|
||||
if (tools.length === 0) return tools
|
||||
const last = tools.length - 1
|
||||
if (tools[last]!.cache || budget.remaining === 0) return tools
|
||||
budget.remaining -= 1
|
||||
return [...tools.slice(0, -1), new ToolDefinition({ ...target, cache: hint })]
|
||||
return tools.map((tool, i) => (i === last ? new ToolDefinition({ ...tool, cache: hint }) : tool))
|
||||
}
|
||||
|
||||
const countToolHints = (tools: ReadonlyArray<ToolEntry>): number =>
|
||||
tools.reduce(
|
||||
(count, tool) => count + (tool.type === "tool" ? (tool.cache === undefined ? 0 : 1) : countToolHints(tool.tools)),
|
||||
0,
|
||||
)
|
||||
|
||||
const markSystemBoundaries = (system: LLMRequest["system"], hint: CacheHint, budget: Budget): LLMRequest["system"] => {
|
||||
if (system.length === 0) return system
|
||||
let changed = false
|
||||
@@ -145,20 +115,14 @@ const markMessages = (
|
||||
return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint, budget)
|
||||
if (strategy === "latest-assistant")
|
||||
return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint, budget)
|
||||
let start = messages.length
|
||||
let remaining = strategy.tail
|
||||
while (remaining > 0 && start > 0) {
|
||||
start -= 1
|
||||
if (effortUpdate(messages[start]!) === undefined) remaining -= 1
|
||||
}
|
||||
const start = Math.max(0, messages.length - strategy.tail)
|
||||
let next = messages
|
||||
for (let i = start; i < messages.length; i++)
|
||||
if (effortUpdate(messages[i]!) === undefined) next = markMessageAt(next, i, hint, budget)
|
||||
for (let i = start; i < messages.length; i++) next = markMessageAt(next, i, hint, budget)
|
||||
return next
|
||||
}
|
||||
|
||||
const countHints = (request: LLMRequest) =>
|
||||
countToolHints(request.tools) +
|
||||
request.tools.reduce((count, tool) => count + (tool.cache === undefined ? 0 : 1), 0) +
|
||||
request.system.reduce((count, part) => count + (part.cache === undefined ? 0 : 1), 0) +
|
||||
request.messages.reduce(
|
||||
(count, message) =>
|
||||
@@ -171,17 +135,10 @@ const countHints = (request: LLMRequest) =>
|
||||
)
|
||||
|
||||
export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
|
||||
const route = request.model.route.id
|
||||
if (!RESPECTS_INLINE_HINTS.has(route)) return request
|
||||
const policy =
|
||||
(route === "openrouter" || route === "vercel-ai-gateway-messages") &&
|
||||
(request.cache === undefined || request.cache === "auto")
|
||||
? gatewayPolicy(request.model.id)
|
||||
: route === "alibaba-chat" && (request.cache === undefined || request.cache === "auto")
|
||||
? request.model.id.toLowerCase().startsWith("qwen")
|
||||
? QWEN
|
||||
: NONE
|
||||
: resolve(request.cache)
|
||||
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
|
||||
if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto"))
|
||||
return request
|
||||
const policy = resolve(request.cache)
|
||||
if (!policy.tools && !policy.system && !policy.messages) return request
|
||||
|
||||
const hint = makeHint(policy.ttlSeconds)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// Changing the top-level reasoning effort invalidates the provider prompt cache. Protocols with a native
|
||||
// per-message update keep it frozen and lower `Message.effort(...)` markers instead; other routes strip them.
|
||||
import { LLMRequest, type EffortPart, type Message } from "./schema/messages.js"
|
||||
|
||||
export const effortUpdate = (message: Message): EffortPart | undefined => {
|
||||
if (message.role !== "system" || message.content.length !== 1) return undefined
|
||||
const part = message.content[0]
|
||||
return part.type === "effort" ? part : undefined
|
||||
}
|
||||
|
||||
export const stripEffortUpdates = (request: LLMRequest) => {
|
||||
const messages = request.messages.filter((message) => effortUpdate(message) === undefined)
|
||||
return messages.length === request.messages.length ? request : LLMRequest.update(request, { messages })
|
||||
}
|
||||
|
||||
export const applyEffortUpdates = (request: LLMRequest): LLMRequest =>
|
||||
request.model.route.supportsEffortUpdates?.(request) ? request : stripEffortUpdates(request)
|
||||
|
||||
// Reverted or forked history can leave the last marker disagreeing with the requested effort.
|
||||
export const resolveEffortUpdates = (request: LLMRequest, current: string | undefined) => {
|
||||
const updates = request.messages.flatMap((message) => effortUpdate(message) ?? [])
|
||||
if (updates.length === 0) return { request, effort: current }
|
||||
if (updates.at(-1)?.effort !== current) return { request: stripEffortUpdates(request), effort: current }
|
||||
return { request, effort: updates[0]?.previous }
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
export { EvaluationClient } from "./experimental/evaluation-client.js"
|
||||
export {
|
||||
BooleanAnswer,
|
||||
BooleanQuestion,
|
||||
ChoiceAnswer,
|
||||
ChoiceQuestion,
|
||||
Evaluation,
|
||||
EvaluationAnswer,
|
||||
EvaluationInput,
|
||||
EvaluationModel,
|
||||
EvaluationModelSchema,
|
||||
EvaluationQuestion,
|
||||
EvaluationRequest,
|
||||
EvaluationResponse,
|
||||
EvaluationRounding,
|
||||
ScoreAnswer,
|
||||
ScoreQuestion,
|
||||
} from "./experimental/evaluation.js"
|
||||
export type {
|
||||
AnswerFor,
|
||||
AnswersFor,
|
||||
EvaluationModelOptions,
|
||||
EvaluationOptions,
|
||||
EvaluationQuestions,
|
||||
EvaluationRequestFor,
|
||||
EvaluationRequestInput,
|
||||
EvaluationResponseFor,
|
||||
EvaluationRoute,
|
||||
} from "./experimental/evaluation.js"
|
||||
@@ -1,96 +0,0 @@
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { RequestExecutor } from "../route/executor.js"
|
||||
import { AIError, InvalidProviderOutputError, mergeHttpOptions } from "../schema/index.js"
|
||||
import { sanitizeSurrogates } from "../utils/sanitize.js"
|
||||
import {
|
||||
type EvaluationOptions,
|
||||
type EvaluationQuestions,
|
||||
type EvaluationRequestFor,
|
||||
type EvaluationResponseFor,
|
||||
} from "./evaluation.js"
|
||||
|
||||
export type Execute = RequestExecutor.Interface["execute"]
|
||||
|
||||
export interface Interface {
|
||||
readonly evaluate: <Options extends EvaluationOptions, const Questions extends EvaluationQuestions>(
|
||||
request: EvaluationRequestFor<Options, Questions>,
|
||||
) => Effect.Effect<EvaluationResponseFor<Questions>, AIError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/AI/Experimental/EvaluationClient") {}
|
||||
|
||||
export const evaluate = <Options extends EvaluationOptions, const Questions extends EvaluationQuestions>(
|
||||
request: EvaluationRequestFor<Options, Questions>,
|
||||
): Effect.Effect<EvaluationResponseFor<Questions>, AIError, Service> =>
|
||||
Effect.flatMap(Service, (client) => client.evaluate(request))
|
||||
|
||||
export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
return Service.of({
|
||||
evaluate: (request) =>
|
||||
request.model.route
|
||||
.evaluate(
|
||||
{
|
||||
...sanitizeSurrogates({
|
||||
...request,
|
||||
model: undefined,
|
||||
http: mergeHttpOptions(request.model.http, request.http),
|
||||
}),
|
||||
model: request.model,
|
||||
},
|
||||
executor.execute,
|
||||
)
|
||||
.pipe(
|
||||
Effect.flatMap((response) => {
|
||||
const questions = Object.entries(request.questions)
|
||||
if (
|
||||
questions.length === Object.keys(response.answers).length &&
|
||||
questions.every(([id, question]) => {
|
||||
const answer = response.answers[id]
|
||||
if (question.type === "boolean") return answer?.type === "boolean"
|
||||
if (question.type === "choice") {
|
||||
if (answer?.type !== "choice" || !Object.hasOwn(question.criteria, answer.choice)) return false
|
||||
if (answer.probabilities === undefined) return true
|
||||
const keys = Object.keys(question.criteria)
|
||||
const probabilities = answer.probabilities
|
||||
return (
|
||||
Object.keys(probabilities).length === keys.length &&
|
||||
keys.every((key) => Object.hasOwn(probabilities, key))
|
||||
)
|
||||
}
|
||||
if (answer?.type !== "score" || answer.score < 0 || answer.score > question.criteria.length - 1)
|
||||
return false
|
||||
if (answer.probabilities === undefined) return true
|
||||
const keys = question.criteria.map((_, index) => String(index))
|
||||
const probabilities = answer.probabilities
|
||||
return (
|
||||
Object.keys(probabilities).length === keys.length &&
|
||||
keys.every((key) => Object.hasOwn(probabilities, key))
|
||||
)
|
||||
})
|
||||
)
|
||||
return Effect.succeed(response as EvaluationResponseFor<typeof request.questions>)
|
||||
return Effect.fail(
|
||||
new AIError({
|
||||
reason: new InvalidProviderOutputError({
|
||||
route: request.model.route.id,
|
||||
message: "Evaluation answers do not match the requested questions",
|
||||
cause: response.answers,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
})
|
||||
}),
|
||||
)
|
||||
export const fetchLayer = layer.pipe(Layer.provideMerge(RequestExecutor.fetchLayer))
|
||||
|
||||
export const EvaluationClient = {
|
||||
Service,
|
||||
layer,
|
||||
fetchLayer,
|
||||
evaluate,
|
||||
} as const
|
||||
@@ -1,248 +0,0 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import {
|
||||
AIError,
|
||||
HttpOptions,
|
||||
InvalidRequestError,
|
||||
ModelID,
|
||||
ProviderID,
|
||||
ProviderMetadata,
|
||||
Usage,
|
||||
} from "../schema/index.js"
|
||||
import { EvaluationClient, Service, type Execute } from "./evaluation-client.js"
|
||||
|
||||
export const EvaluationInput = Schema.Union([Schema.String, Schema.JsonObject, Schema.Array(Schema.Json)])
|
||||
export type EvaluationInput = Schema.Schema.Type<typeof EvaluationInput>
|
||||
|
||||
const EvaluationCriterion = Schema.NullOr(EvaluationInput)
|
||||
const ChoiceCriteria = Schema.Record(Schema.String, EvaluationCriterion).pipe(
|
||||
Schema.refine((x): x is typeof x => Object.keys(x).length > 0, {
|
||||
message: "Choice criteria must be a nonempty option map",
|
||||
}),
|
||||
)
|
||||
|
||||
export const ChoiceQuestion = Schema.Struct({
|
||||
type: Schema.Literal("choice"),
|
||||
instructions: EvaluationInput,
|
||||
criteria: ChoiceCriteria,
|
||||
})
|
||||
export type ChoiceQuestion = Schema.Schema.Type<typeof ChoiceQuestion>
|
||||
|
||||
export const ScoreQuestion = Schema.Struct({
|
||||
type: Schema.Literal("score"),
|
||||
instructions: EvaluationInput,
|
||||
criteria: Schema.Array(EvaluationCriterion).check(Schema.isMinLength(2)),
|
||||
})
|
||||
export type ScoreQuestion = Schema.Schema.Type<typeof ScoreQuestion>
|
||||
|
||||
export const BooleanQuestion = Schema.Struct({
|
||||
type: Schema.Literal("boolean"),
|
||||
instructions: EvaluationInput,
|
||||
criteria: Schema.optional(
|
||||
Schema.Struct({
|
||||
true: Schema.optional(EvaluationCriterion),
|
||||
false: Schema.optional(EvaluationCriterion),
|
||||
}),
|
||||
),
|
||||
})
|
||||
export type BooleanQuestion = Schema.Schema.Type<typeof BooleanQuestion>
|
||||
|
||||
export const EvaluationQuestion = Schema.Union([ChoiceQuestion, ScoreQuestion, BooleanQuestion]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
)
|
||||
export type EvaluationQuestion = Schema.Schema.Type<typeof EvaluationQuestion>
|
||||
export type EvaluationQuestions = Readonly<Record<string, EvaluationQuestion>>
|
||||
const EvaluationQuestions = Schema.Record(Schema.String, EvaluationQuestion).pipe(
|
||||
Schema.refine((x): x is typeof x => Object.keys(x).length > 0, {
|
||||
message: "Evaluation questions must be a nonempty map",
|
||||
}),
|
||||
)
|
||||
|
||||
const Probability = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))
|
||||
|
||||
export const ChoiceAnswer = Schema.Struct({
|
||||
type: Schema.Literal("choice"),
|
||||
choice: Schema.String,
|
||||
probabilities: Schema.optional(Schema.Record(Schema.String, Probability)),
|
||||
confidence: Schema.optional(Probability),
|
||||
})
|
||||
export type ChoiceAnswer = Schema.Schema.Type<typeof ChoiceAnswer>
|
||||
|
||||
export const ScoreAnswer = Schema.Struct({
|
||||
type: Schema.Literal("score"),
|
||||
score: Schema.Number,
|
||||
probabilities: Schema.optional(Schema.Record(Schema.String, Probability)),
|
||||
confidence: Schema.optional(Probability),
|
||||
})
|
||||
export type ScoreAnswer = Schema.Schema.Type<typeof ScoreAnswer>
|
||||
|
||||
export const BooleanAnswer = Schema.Struct({
|
||||
type: Schema.Literal("boolean"),
|
||||
probability: Probability,
|
||||
})
|
||||
export type BooleanAnswer = Schema.Schema.Type<typeof BooleanAnswer>
|
||||
|
||||
export const EvaluationAnswer = Schema.Union([ChoiceAnswer, ScoreAnswer, BooleanAnswer]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
)
|
||||
export type EvaluationAnswer = Schema.Schema.Type<typeof EvaluationAnswer>
|
||||
|
||||
export type AnswerFor<Question extends EvaluationQuestion> = Question extends {
|
||||
readonly type: "choice"
|
||||
readonly criteria: infer Criteria
|
||||
}
|
||||
? {
|
||||
readonly type: "choice"
|
||||
readonly choice: Extract<keyof Criteria, string>
|
||||
readonly probabilities?: Readonly<Record<Extract<keyof Criteria, string>, number>>
|
||||
readonly confidence?: number
|
||||
}
|
||||
: Question extends { readonly type: "score" }
|
||||
? ScoreAnswer
|
||||
: BooleanAnswer
|
||||
|
||||
export type AnswersFor<Questions extends EvaluationQuestions> = {
|
||||
readonly [ID in keyof Questions]: AnswerFor<Questions[ID]>
|
||||
}
|
||||
|
||||
export type EvaluationOptions = Record<string, unknown>
|
||||
|
||||
export interface EvaluationRoute<Options extends EvaluationOptions = EvaluationOptions> {
|
||||
readonly id: string
|
||||
readonly evaluate: (
|
||||
request: EvaluationRequestFor<Options>,
|
||||
execute: Execute,
|
||||
) => Effect.Effect<EvaluationResponse, AIError>
|
||||
}
|
||||
|
||||
export class EvaluationModel<Options extends EvaluationOptions = EvaluationOptions> {
|
||||
declare protected readonly _Options: (options: Options) => Options
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: EvaluationRoute<Options>
|
||||
readonly http?: HttpOptions
|
||||
|
||||
constructor(input: EvaluationModel.Input<Options>) {
|
||||
this.id = input.id
|
||||
this.provider = input.provider
|
||||
this.route = input.route
|
||||
this.http = input.http
|
||||
}
|
||||
|
||||
static make<Options extends EvaluationOptions = EvaluationOptions>(input: EvaluationModel.MakeInput<Options>) {
|
||||
return new EvaluationModel<Options>({
|
||||
id: ModelID.make(input.id),
|
||||
provider: ProviderID.make(input.provider),
|
||||
route: input.route,
|
||||
http: input.http,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export namespace EvaluationModel {
|
||||
export interface Input<Options extends EvaluationOptions = EvaluationOptions> {
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: EvaluationRoute<Options>
|
||||
readonly http?: HttpOptions
|
||||
}
|
||||
|
||||
export interface MakeInput<Options extends EvaluationOptions = EvaluationOptions>
|
||||
extends Omit<Input<Options>, "id" | "provider"> {
|
||||
readonly id: string | ModelID
|
||||
readonly provider: string | ProviderID
|
||||
}
|
||||
}
|
||||
|
||||
export const EvaluationModelSchema = Schema.declare(
|
||||
(value): value is EvaluationModel => value instanceof EvaluationModel,
|
||||
{
|
||||
expected: "Evaluation.Model",
|
||||
},
|
||||
)
|
||||
|
||||
export class EvaluationRequest extends Schema.Class<EvaluationRequest>("Evaluation.Request")({
|
||||
model: EvaluationModelSchema,
|
||||
state: EvaluationInput,
|
||||
questions: EvaluationQuestions,
|
||||
options: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
http: Schema.optional(HttpOptions),
|
||||
}) {
|
||||
declare protected readonly _EvaluationRequest: void
|
||||
}
|
||||
|
||||
export type EvaluationModelOptions<Model> = Model extends EvaluationModel<infer Options> ? Options : never
|
||||
|
||||
export type EvaluationRequestFor<
|
||||
Options extends EvaluationOptions = EvaluationOptions,
|
||||
Questions extends EvaluationQuestions = EvaluationQuestions,
|
||||
> = Omit<EvaluationRequest, "model" | "questions" | "options"> & {
|
||||
readonly model: EvaluationModel<Options>
|
||||
readonly questions: Questions
|
||||
readonly options?: Options
|
||||
}
|
||||
|
||||
export type EvaluationRequestInput<
|
||||
Model extends object = EvaluationModel,
|
||||
Questions extends EvaluationQuestions = EvaluationQuestions,
|
||||
> = Omit<ConstructorParameters<typeof EvaluationRequest>[0], "model" | "questions" | "options" | "http"> & {
|
||||
readonly model: Model
|
||||
readonly questions: Questions
|
||||
readonly options?: NoInfer<EvaluationModelOptions<Model>>
|
||||
readonly http?: HttpOptions.Input
|
||||
} & (Model extends EvaluationModel<EvaluationModelOptions<Model>> ? unknown : never)
|
||||
|
||||
export class EvaluationRounding extends Schema.Class<EvaluationRounding>("Evaluation.Rounding")({
|
||||
probabilityDecimals: Schema.optional(Schema.Int),
|
||||
scoreDecimals: Schema.optional(Schema.Int),
|
||||
}) {}
|
||||
|
||||
export class EvaluationResponse extends Schema.Class<EvaluationResponse>("Evaluation.Response")({
|
||||
model: ModelID,
|
||||
answers: Schema.Record(Schema.String, EvaluationAnswer),
|
||||
usage: Schema.optional(Usage),
|
||||
rounding: Schema.optional(EvaluationRounding),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}) {}
|
||||
|
||||
export type EvaluationResponseFor<Questions extends EvaluationQuestions> = Omit<EvaluationResponse, "answers"> & {
|
||||
readonly answers: AnswersFor<Questions>
|
||||
}
|
||||
|
||||
export function request<const Model extends object, const Questions extends EvaluationQuestions>(
|
||||
input: EvaluationRequestInput<Model, Questions>,
|
||||
): EvaluationRequestFor<EvaluationModelOptions<Model>, Questions>
|
||||
export function request(input: EvaluationRequest): EvaluationRequest
|
||||
export function request(input: EvaluationRequest | EvaluationRequestInput) {
|
||||
if (input instanceof EvaluationRequest) return input
|
||||
return new EvaluationRequest({
|
||||
...input,
|
||||
model: input.model as unknown as EvaluationModel,
|
||||
http: HttpOptions.make(input.http),
|
||||
})
|
||||
}
|
||||
|
||||
export function run<const Model extends object, const Questions extends EvaluationQuestions>(
|
||||
input: EvaluationRequestInput<Model, Questions>,
|
||||
): Effect.Effect<EvaluationResponseFor<Questions>, AIError, Service>
|
||||
export function run(input: EvaluationRequest): Effect.Effect<EvaluationResponse, AIError, Service>
|
||||
export function run(input: EvaluationRequest | EvaluationRequestInput) {
|
||||
return Effect.try({
|
||||
try: () => (input instanceof EvaluationRequest ? input : request(input)),
|
||||
catch: (cause) =>
|
||||
new AIError({
|
||||
reason: new InvalidRequestError({
|
||||
message: cause instanceof Error ? cause.message : String(cause),
|
||||
cause,
|
||||
}),
|
||||
}),
|
||||
}).pipe(
|
||||
Effect.flatMap((request) =>
|
||||
EvaluationClient.evaluate(request as EvaluationRequestFor<EvaluationOptions, EvaluationQuestions>),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const Evaluation = {
|
||||
request,
|
||||
run,
|
||||
} as const
|
||||
@@ -1,199 +0,0 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
ChoiceQuestion,
|
||||
EvaluationInput,
|
||||
EvaluationModel,
|
||||
EvaluationResponse,
|
||||
EvaluationRounding,
|
||||
ScoreQuestion,
|
||||
type EvaluationAnswer,
|
||||
type EvaluationOptions,
|
||||
} from "./evaluation.js"
|
||||
import { Auth, type Definition as AuthDefinition } from "../route/auth.js"
|
||||
import {
|
||||
AIError,
|
||||
HttpContext,
|
||||
HttpOptions,
|
||||
InvalidProviderOutputError,
|
||||
InvalidRequestError,
|
||||
ModelID,
|
||||
Usage,
|
||||
mergeJsonRecords,
|
||||
} from "../schema/index.js"
|
||||
|
||||
const Noul = Schema.Struct({
|
||||
type: Schema.Literal("noul"),
|
||||
instructions: EvaluationInput,
|
||||
criteria: Schema.optional(
|
||||
Schema.Struct({
|
||||
true: Schema.optional(Schema.NullOr(EvaluationInput)),
|
||||
false: Schema.optional(Schema.NullOr(EvaluationInput)),
|
||||
}),
|
||||
),
|
||||
})
|
||||
const Question = Schema.Union([
|
||||
ChoiceQuestion.pipe(
|
||||
Schema.refine((x): x is typeof x => Object.keys(x.criteria).length <= 255, {
|
||||
message: "System One Choice questions support at most 255 options",
|
||||
}),
|
||||
),
|
||||
ScoreQuestion.pipe(
|
||||
Schema.refine((x): x is typeof x => x.criteria.length <= 10, {
|
||||
message: "System One Score questions support at most 10 levels",
|
||||
}),
|
||||
),
|
||||
Noul,
|
||||
])
|
||||
const Request = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
model: Schema.String,
|
||||
state: EvaluationInput,
|
||||
questions: Schema.Record(Schema.String, Question),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Any)],
|
||||
)
|
||||
|
||||
const Probability = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))
|
||||
const NoulAnswer = Schema.Struct({ type: Schema.Literal("noul"), noul: Probability })
|
||||
const Choice = Schema.Struct({
|
||||
type: Schema.Literal("choice"),
|
||||
choice: Schema.String,
|
||||
probabilities: Schema.Record(Schema.String, Probability),
|
||||
confidence: Schema.optional(Probability),
|
||||
})
|
||||
const Score = Schema.Struct({
|
||||
type: Schema.Literal("score"),
|
||||
score: Schema.Number,
|
||||
probabilities: Schema.Record(Schema.String, Probability),
|
||||
legend: Schema.optional(Schema.Record(Schema.String, Schema.Json)),
|
||||
confidence: Schema.optional(Probability),
|
||||
})
|
||||
const Answer = Schema.Union([NoulAnswer, Choice, Score]).pipe(Schema.toTaggedUnion("type"))
|
||||
const NativeUsage = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
input_tokens: Schema.optional(Schema.Number),
|
||||
output_tokens: Schema.optional(Schema.Number),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
const Response = Schema.Struct({
|
||||
model: Schema.String,
|
||||
answers: Schema.Record(Schema.String, Answer),
|
||||
usage: Schema.optional(NativeUsage),
|
||||
id: Schema.optional(Schema.String),
|
||||
provider: Schema.optional(Schema.String),
|
||||
provider_metadata: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))),
|
||||
})
|
||||
|
||||
export interface ModelInput {
|
||||
readonly id: string | ModelID
|
||||
readonly provider: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly auth: AuthDefinition
|
||||
readonly baseURL: string
|
||||
readonly headers?: Record<string, string>
|
||||
readonly http?: HttpOptions
|
||||
}
|
||||
|
||||
export const model = <Options extends EvaluationOptions = EvaluationOptions>(cfg: ModelInput) =>
|
||||
EvaluationModel.make<Options>({
|
||||
id: cfg.id,
|
||||
provider: cfg.provider,
|
||||
http: cfg.http,
|
||||
route: {
|
||||
id: "system-one",
|
||||
evaluate: (req, send) =>
|
||||
Effect.gen(function* () {
|
||||
const url = new URL(`${cfg.baseURL.replace(/\/$/, "")}/systemone`)
|
||||
Object.entries(req.http?.query ?? {}).forEach(([key, value]) => url.searchParams.set(key, value))
|
||||
const body = yield* Schema.encodeUnknownEffect(Schema.fromJsonString(Request))({
|
||||
...mergeJsonRecords(req.options, req.http?.body),
|
||||
model: req.model.id,
|
||||
state: req.state,
|
||||
questions: Object.fromEntries(
|
||||
Object.entries(req.questions).map(([id, x]) => [id, x.type === "boolean" ? { ...x, type: "noul" } : x]),
|
||||
),
|
||||
}).pipe(
|
||||
Effect.mapError(
|
||||
(cause) => new AIError({ reason: new InvalidRequestError({ message: cause.message, cause }) }),
|
||||
),
|
||||
)
|
||||
const headers = yield* Auth.toEffect(cfg.auth)({
|
||||
request: req,
|
||||
method: "POST",
|
||||
url: url.toString(),
|
||||
body,
|
||||
headers: Headers.fromInput({ ...cfg.headers, ...req.http?.headers }),
|
||||
})
|
||||
const res = yield* send(
|
||||
HttpClientRequest.post(url).pipe(
|
||||
HttpClientRequest.setHeaders(headers),
|
||||
HttpClientRequest.bodyText(body, "application/json"),
|
||||
),
|
||||
)
|
||||
const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers })
|
||||
const fail = (message: string, cause: unknown, body?: string) =>
|
||||
new AIError({ reason: new InvalidProviderOutputError({ route: "system-one", message, body, http, cause }) })
|
||||
const text = yield* res.text.pipe(
|
||||
Effect.mapError((cause) => fail("Failed to read the System One response", cause)),
|
||||
)
|
||||
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Response))(text).pipe(
|
||||
Effect.mapError((cause) => fail("System One returned an invalid response", cause, text)),
|
||||
)
|
||||
|
||||
const legend: Record<string, Record<string, Schema.Json>> = {}
|
||||
const answers = Object.fromEntries(
|
||||
Object.entries(data.answers).map(([id, answer]): [string, EvaluationAnswer] => {
|
||||
if (answer.type === "noul") return [id, { type: "boolean", probability: answer.noul }]
|
||||
if (answer.type === "choice") {
|
||||
return [
|
||||
id,
|
||||
{
|
||||
type: "choice",
|
||||
choice: answer.choice,
|
||||
probabilities: answer.probabilities,
|
||||
...(answer.confidence === undefined ? {} : { confidence: answer.confidence }),
|
||||
},
|
||||
]
|
||||
}
|
||||
if (answer.legend !== undefined) legend[id] = answer.legend
|
||||
return [
|
||||
id,
|
||||
{
|
||||
type: "score",
|
||||
score: answer.score,
|
||||
probabilities: answer.probabilities,
|
||||
...(answer.confidence === undefined ? {} : { confidence: answer.confidence }),
|
||||
},
|
||||
]
|
||||
}),
|
||||
)
|
||||
const meta = {
|
||||
...(data.id === undefined ? {} : { responseId: data.id }),
|
||||
...(data.provider === undefined ? {} : { provider: data.provider }),
|
||||
...data.provider_metadata?.[cfg.providerMetadataKey],
|
||||
...(Object.keys(legend).length === 0 ? {} : { legend }),
|
||||
}
|
||||
return new EvaluationResponse({
|
||||
model: ModelID.make(data.model),
|
||||
answers,
|
||||
usage: data.usage
|
||||
? new Usage({
|
||||
inputTokens: data.usage.input_tokens,
|
||||
outputTokens: data.usage.output_tokens,
|
||||
totalTokens:
|
||||
data.usage.input_tokens === undefined && data.usage.output_tokens === undefined
|
||||
? undefined
|
||||
: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0),
|
||||
providerMetadata: { [cfg.providerMetadataKey]: data.usage },
|
||||
})
|
||||
: undefined,
|
||||
rounding: new EvaluationRounding({ probabilityDecimals: 2, scoreDecimals: 2 }),
|
||||
providerMetadata: Object.keys(meta).length === 0 ? undefined : { [cfg.providerMetadataKey]: meta },
|
||||
})
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const SystemOne = { model } as const
|
||||
@@ -1,207 +0,0 @@
|
||||
import { Clock, Duration, Effect, Schedule, Schema, Stream } from "effect"
|
||||
import { AIError, TimeoutError } from "./schema/errors.js"
|
||||
|
||||
export const Status = Schema.Literals(["queued", "running", "completed", "failed", "cancelled", "expired"])
|
||||
export type Status = Schema.Schema.Type<typeof Status>
|
||||
|
||||
/** Provider-neutral view of one generation observation. */
|
||||
export interface Snapshot {
|
||||
readonly id: string
|
||||
readonly status: Status
|
||||
/** Normalized 0..1 when the provider reports progress. */
|
||||
readonly progress?: number
|
||||
readonly position?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Route-owned generation operations for one generation. The media route decodes its serializable token once (from the
|
||||
* submission response or a `resume` input) and closes over it, so `Generation` never sees the token's shape.
|
||||
*/
|
||||
export interface Route<Response> {
|
||||
readonly status: Effect.Effect<Snapshot, AIError>
|
||||
readonly result: Effect.Effect<Response, AIError>
|
||||
readonly cancel?: Effect.Effect<void, AIError>
|
||||
}
|
||||
|
||||
export interface Poll {
|
||||
readonly interval?: Duration.Input
|
||||
readonly timeout?: Duration.Input
|
||||
}
|
||||
|
||||
export interface AwaitOptions {
|
||||
readonly poll?: Poll
|
||||
}
|
||||
|
||||
export const DEFAULT_POLL_INTERVAL = Duration.seconds(5)
|
||||
export const DEFAULT_POLL_TIMEOUT = Duration.minutes(10)
|
||||
|
||||
export const QueuedEvent = Schema.Struct({
|
||||
type: Schema.tag("generation-queued"),
|
||||
id: Schema.String,
|
||||
position: Schema.optional(Schema.Number),
|
||||
}).annotate({ identifier: "Generation.Event.Queued" })
|
||||
|
||||
export const ProgressEvent = Schema.Struct({
|
||||
type: Schema.tag("generation-progress"),
|
||||
id: Schema.String,
|
||||
progress: Schema.optional(Schema.Number),
|
||||
}).annotate({ identifier: "Generation.Event.Progress" })
|
||||
|
||||
export type Observation = Schema.Schema.Type<typeof QueuedEvent> | Schema.Schema.Type<typeof ProgressEvent>
|
||||
|
||||
export type Event = Observation | { readonly type: "generation-finished"; readonly id: string; readonly status: Status }
|
||||
|
||||
const TERMINAL: ReadonlySet<Status> = new Set(["completed", "failed", "cancelled", "expired"])
|
||||
|
||||
export const isTerminal = (status: Status) => TERMINAL.has(status)
|
||||
|
||||
export class Generation<Response> {
|
||||
readonly id: string
|
||||
readonly status: Status
|
||||
readonly progress?: number
|
||||
readonly position?: number
|
||||
|
||||
constructor(
|
||||
readonly route: Route<Response>,
|
||||
/** Route-owned serializable JSON; pass it to the modality's `resume` from another process. */
|
||||
readonly token: unknown,
|
||||
snapshot: Snapshot,
|
||||
) {
|
||||
this.id = snapshot.id
|
||||
this.status = snapshot.status
|
||||
this.progress = snapshot.progress
|
||||
this.position = snapshot.position
|
||||
}
|
||||
|
||||
get snapshot(): Snapshot {
|
||||
return {
|
||||
id: this.id,
|
||||
status: this.status,
|
||||
progress: this.progress,
|
||||
position: this.position,
|
||||
}
|
||||
}
|
||||
|
||||
get terminal() {
|
||||
return isTerminal(this.status)
|
||||
}
|
||||
|
||||
refresh(): Effect.Effect<Generation<Response>, AIError> {
|
||||
return this.route.status.pipe(Effect.map((snapshot) => new Generation(this.route, this.token, snapshot)))
|
||||
}
|
||||
|
||||
/** Fetch the result without polling; non-completed terminal generations fail with the provider's terminal body. */
|
||||
result(): Effect.Effect<Response, AIError> {
|
||||
return this.route.result
|
||||
}
|
||||
|
||||
/** Poll until the generation reaches a terminal status, then fetch the result. Fails with a `Timeout` reason on deadline. */
|
||||
await(options?: AwaitOptions): Effect.Effect<Response, AIError> {
|
||||
const timeout = Duration.fromInputUnsafe(options?.poll?.timeout ?? DEFAULT_POLL_TIMEOUT)
|
||||
const settled = this.terminal ? Effect.succeed(this) : this.poll(options?.poll)
|
||||
return settled.pipe(
|
||||
// Non-completed terminal states also go through `result` so the route can surface its provider failure body.
|
||||
Effect.flatMap((generation) => generation.result()),
|
||||
Effect.timeoutOrElse({ duration: timeout, orElse: () => timeoutError(this.id, timeout) }),
|
||||
)
|
||||
}
|
||||
|
||||
cancel(): Effect.Effect<void, AIError> {
|
||||
return this.route.cancel ?? Effect.void
|
||||
}
|
||||
|
||||
/**
|
||||
* Status observations as a stream, ending after the first terminal observation. Each poll and each sleep between polls
|
||||
* is bounded by the time remaining until `poll.timeout`, so a hung status request or a long interval fails the stream at
|
||||
* the deadline instead of stalling it. (`Stream.interruptWhen` would express this directly but deadlocks under
|
||||
* `TestClock` when the source completes while the timer sleeps.)
|
||||
*/
|
||||
events(options?: AwaitOptions): Stream.Stream<Event, AIError> {
|
||||
if (this.terminal) return Stream.make(this.event())
|
||||
const timeout = Duration.fromInputUnsafe(options?.poll?.timeout ?? DEFAULT_POLL_TIMEOUT)
|
||||
return Stream.unwrap(
|
||||
Clock.currentTimeMillis.pipe(
|
||||
Effect.map((start) => {
|
||||
const deadline = start + Duration.toMillis(timeout)
|
||||
const refresh = within(this.refresh(), this.id, timeout, deadline)
|
||||
const schedule = this.schedule(options?.poll).pipe(
|
||||
Schedule.modifyDelay((meta) =>
|
||||
Effect.succeed(Duration.min(meta.duration, Duration.millis(Math.max(0, deadline - meta.now)))),
|
||||
),
|
||||
)
|
||||
return Stream.fromEffectSchedule(refresh, schedule).pipe(
|
||||
Stream.takeUntil((generation) => generation.terminal),
|
||||
Stream.map((generation) => generation.event()),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private event(): Event {
|
||||
if (this.terminal) return { type: "generation-finished", id: this.id, status: this.status }
|
||||
if (this.status === "queued") return { type: "generation-queued", id: this.id, position: this.position }
|
||||
return { type: "generation-progress", id: this.id, progress: this.progress }
|
||||
}
|
||||
|
||||
private poll(poll: Poll | undefined) {
|
||||
return this.refresh().pipe(
|
||||
Effect.repeat({ schedule: this.schedule(poll), until: (generation) => generation.terminal }),
|
||||
)
|
||||
}
|
||||
|
||||
private schedule(poll: Poll | undefined) {
|
||||
return Schedule.spaced(poll?.interval ?? DEFAULT_POLL_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
/** `events` followed by the expanded result, with the result fetch bounded by the same `poll.timeout` deadline. */
|
||||
export const resultEvents = <Response, A>(
|
||||
generation: Generation<Response>,
|
||||
expand: (response: Response) => ReadonlyArray<A>,
|
||||
options?: AwaitOptions,
|
||||
): Stream.Stream<Observation | A, AIError> => {
|
||||
const timeout = Duration.fromInputUnsafe(options?.poll?.timeout ?? DEFAULT_POLL_TIMEOUT)
|
||||
return Stream.unwrap(
|
||||
Clock.currentTimeMillis.pipe(
|
||||
Effect.map((start) =>
|
||||
generation.events(options).pipe(
|
||||
Stream.filter((event): event is Observation => event.type !== "generation-finished"),
|
||||
Stream.concat(
|
||||
Stream.fromIterableEffect(
|
||||
within(generation.result(), generation.id, timeout, start + Duration.toMillis(timeout)).pipe(
|
||||
Effect.map(expand),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `effect` within the time left until `deadline`. Fails before starting once the deadline has passed: a fast
|
||||
* request could otherwise win the zero-budget race and schedule another zero-delay poll.
|
||||
*/
|
||||
const within = <A>(effect: Effect.Effect<A, AIError>, id: string, timeout: Duration.Duration, deadline: number) =>
|
||||
Clock.currentTimeMillis.pipe(
|
||||
Effect.flatMap((now) =>
|
||||
now >= deadline
|
||||
? Effect.fail(timeoutError(id, timeout))
|
||||
: effect.pipe(
|
||||
Effect.timeoutOrElse({
|
||||
duration: Duration.millis(deadline - now),
|
||||
orElse: () => Effect.fail(timeoutError(id, timeout)),
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const timeoutError = (id: string, timeout: Duration.Duration) =>
|
||||
new AIError({
|
||||
reason: new TimeoutError({
|
||||
message: `Generation ${id} did not finish within ${Duration.format(timeout)}`,
|
||||
timeoutMs: Duration.toMillis(timeout),
|
||||
}),
|
||||
})
|
||||
@@ -1,30 +1,50 @@
|
||||
import { Context } from "effect"
|
||||
import { MediaClient } from "./media-client.js"
|
||||
import {
|
||||
ImageOutputEvent,
|
||||
ImageFinishEvent,
|
||||
type ImageEvent,
|
||||
type ImageRequestFor,
|
||||
type ImageResponse,
|
||||
} from "./image.js"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { RequestExecutor } from "./route/executor.js"
|
||||
import { mergeHttpOptions, type AIError } from "./schema/index.js"
|
||||
import { sanitizeSurrogates } from "./utils/sanitize.js"
|
||||
import type { ImageOptions, ImageRequest, ImageRequestFor, ImageResponse } from "./image.js"
|
||||
|
||||
export type Interface = MediaClient.Interface<ImageRequestFor, ImageEvent, ImageResponse>
|
||||
export type Execute = RequestExecutor.Interface["execute"]
|
||||
|
||||
export class ImageClientService extends Context.Service<ImageClientService, Interface>()("@opencode/ImageClient") {}
|
||||
export const Service = ImageClientService
|
||||
export type Service = ImageClientService
|
||||
export interface Interface {
|
||||
readonly generate: <Options extends ImageOptions>(
|
||||
request: ImageRequestFor<Options>,
|
||||
) => Effect.Effect<ImageResponse, AIError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ImageClient") {}
|
||||
|
||||
export const generate = <Options extends ImageOptions>(
|
||||
request: ImageRequestFor<Options>,
|
||||
): Effect.Effect<ImageResponse, AIError, Service> =>
|
||||
Effect.gen(function* () {
|
||||
const client = yield* Service
|
||||
return yield* client.generate(request)
|
||||
})
|
||||
|
||||
export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
return Service.of({
|
||||
generate: (request) =>
|
||||
request.model.route.generate(
|
||||
{
|
||||
...sanitizeSurrogates({
|
||||
...request,
|
||||
model: undefined,
|
||||
http: mergeHttpOptions(request.model.http, request.http),
|
||||
}),
|
||||
model: request.model,
|
||||
},
|
||||
executor.execute,
|
||||
),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const ImageClient = {
|
||||
Service,
|
||||
...MediaClient.make(Service, {
|
||||
modality: "image",
|
||||
responseEvents: (response: ImageResponse) => [
|
||||
...response.images.map((image, index) => ImageOutputEvent.make({ index, image })),
|
||||
ImageFinishEvent.make({
|
||||
usage: response.usage,
|
||||
notices: response.notices,
|
||||
providerMetadata: response.providerMetadata,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
layer,
|
||||
generate,
|
||||
} as const
|
||||
+109
-159
@@ -1,113 +1,134 @@
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { Generation, ProgressEvent, QueuedEvent, type AwaitOptions } from "./generation.js"
|
||||
import { Media } from "./media.js"
|
||||
import { MediaModel, composeRoute, tryRequest } from "./media-model.js"
|
||||
import { MediaRoute } from "./route/media.js"
|
||||
import { AIError, HttpOptions, MediaUsage, ProviderMetadata, type OpenString } from "./schema/index.js"
|
||||
import { ImageClient, Service } from "./image-client.js"
|
||||
import { Effect, Schema } from "effect"
|
||||
import {
|
||||
HttpOptions,
|
||||
InvalidRequestReason,
|
||||
AIError,
|
||||
ModelID,
|
||||
ProviderID,
|
||||
ProviderMetadata,
|
||||
Usage,
|
||||
} from "./schema/index.js"
|
||||
import { ImageClient, Service, type Execute as ImageExecute } from "./image-client.js"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Model
|
||||
// ---------------------------------------------------------------------------
|
||||
export interface ImageRoute<Options extends ImageOptions = ImageOptions> {
|
||||
readonly id: string
|
||||
readonly generate: (request: ImageRequestFor<Options>, execute: ImageExecute) => Effect.Effect<ImageResponse, AIError>
|
||||
}
|
||||
|
||||
export type ImageOptions = MediaModel.Options
|
||||
export type ImageOptions = Record<string, unknown>
|
||||
|
||||
export type ImageRoute = MediaRoute.AnyRoute<ImageRequestFor, ImageEvent, ImageResponse>
|
||||
export class ImageModel<Options extends ImageOptions = ImageOptions> {
|
||||
declare protected readonly _Options: (options: Options) => Options
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: ImageRoute<Options>
|
||||
readonly http?: HttpOptions
|
||||
|
||||
export class ImageModel<Options extends ImageOptions = ImageOptions> extends MediaModel<ImageRoute, Options> {
|
||||
declare protected readonly _ImageModel: void
|
||||
constructor(input: ImageModel.Input<Options>) {
|
||||
this.id = input.id
|
||||
this.provider = input.provider
|
||||
this.route = input.route
|
||||
this.http = input.http
|
||||
}
|
||||
|
||||
/** The number of type arguments selects the kind: `<Options>`, `<Options, Frame, State>`, or `<Options, Token>`. */
|
||||
static fromRoute<Options extends ImageOptions>(
|
||||
route: MediaModel.InlineRouteInput<ImageRequestFor<Options>, ImageResponse>,
|
||||
input: MediaRoute.ModelInput,
|
||||
): ImageModel<Options>
|
||||
static fromRoute<Options extends ImageOptions, Frame, State>(
|
||||
route: MediaModel.StreamRouteInput<ImageRequestFor<Options>, ImageEvent, Frame, State>,
|
||||
input: MediaRoute.ModelInput,
|
||||
): ImageModel<Options>
|
||||
static fromRoute<Options extends ImageOptions, Token>(
|
||||
route: MediaModel.QueuedRouteInput<ImageRequestFor<Options>, ImageResponse, Token>,
|
||||
input: MediaRoute.ModelInput,
|
||||
): ImageModel<Options>
|
||||
static fromRoute<Options extends ImageOptions, Frame, State, Token>(
|
||||
route: MediaModel.AnyRouteInput<ImageRequestFor<Options>, ImageEvent, ImageResponse, Frame, State, Token>,
|
||||
input: MediaRoute.ModelInput,
|
||||
) {
|
||||
static make<Options extends ImageOptions = ImageOptions>(input: ImageModel.MakeInput<Options>) {
|
||||
return new ImageModel<Options>({
|
||||
id: input.id,
|
||||
provider: route.protocol.provider,
|
||||
id: ModelID.make(input.id),
|
||||
provider: ProviderID.make(input.provider),
|
||||
route: input.route,
|
||||
http: input.http,
|
||||
route: composeRoute(route, input, collectResponse) as ImageRoute,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ImageModel {
|
||||
export interface Input<Options extends ImageOptions = ImageOptions> {
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: ImageRoute<Options>
|
||||
readonly http?: HttpOptions
|
||||
}
|
||||
|
||||
export interface MakeInput<Options extends ImageOptions = ImageOptions>
|
||||
extends Omit<Input<Options>, "id" | "provider"> {
|
||||
readonly id: string | ModelID
|
||||
readonly provider: string | ProviderID
|
||||
}
|
||||
}
|
||||
|
||||
export const ImageModelSchema = Schema.declare((value): value is ImageModel => value instanceof ImageModel, {
|
||||
expected: "Image.Model",
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Request
|
||||
// ---------------------------------------------------------------------------
|
||||
const ImageBytesInput = Schema.Struct({
|
||||
type: Schema.Literal("bytes"),
|
||||
data: Schema.Uint8Array,
|
||||
mediaType: Schema.String,
|
||||
})
|
||||
const ImageUrlInput = Schema.Struct({
|
||||
type: Schema.Literal("url"),
|
||||
url: Schema.String,
|
||||
})
|
||||
const ImageFileIDInput = Schema.Struct({
|
||||
type: Schema.Literal("file-id"),
|
||||
id: Schema.String,
|
||||
})
|
||||
const ImageFileURIInput = Schema.Struct({
|
||||
type: Schema.Literal("file-uri"),
|
||||
uri: Schema.String,
|
||||
mediaType: Schema.String,
|
||||
})
|
||||
|
||||
export type ImageSize = `${number}x${number}`
|
||||
export const ImageSize = Schema.declare<ImageSize>(
|
||||
(value): value is ImageSize => typeof value === "string" && /^\d+x\d+$/.test(value),
|
||||
{ title: "ImageSize" },
|
||||
)
|
||||
export const ImageInputSchema = Schema.Union([
|
||||
ImageBytesInput,
|
||||
ImageUrlInput,
|
||||
ImageFileIDInput,
|
||||
ImageFileURIInput,
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type ImageInput = Schema.Schema.Type<typeof ImageInputSchema>
|
||||
|
||||
export type ImageAspectRatio = Media.AspectRatio
|
||||
export const ImageAspectRatio = Media.AspectRatio
|
||||
|
||||
export type ImageFormat = OpenString<"png" | "jpeg" | "webp">
|
||||
export const ImageInput = {
|
||||
bytes: (data: Uint8Array, mediaType: string): ImageInput => ({ type: "bytes", data, mediaType }),
|
||||
url: (url: string): ImageInput => ({ type: "url", url }),
|
||||
file: (id: string): ImageInput => ({ type: "file-id", id }),
|
||||
fileUri: (uri: string, mediaType: string): ImageInput => ({ type: "file-uri", uri, mediaType }),
|
||||
} as const
|
||||
|
||||
export class ImageRequest extends Schema.Class<ImageRequest>("Image.Request")({
|
||||
model: ImageModelSchema,
|
||||
prompt: Schema.String,
|
||||
/** Edit sources or style/subject references, in order. */
|
||||
images: Schema.optional(Schema.Array(Media.AssetSchema)),
|
||||
/** Inpainting mask; routes that cannot honor it fail with `UnsupportedOperation`. */
|
||||
mask: Schema.optional(Media.AssetSchema),
|
||||
n: Schema.optional(Schema.Int),
|
||||
size: Schema.optional(ImageSize),
|
||||
aspectRatio: Schema.optional(ImageAspectRatio),
|
||||
seed: Schema.optional(Schema.Number),
|
||||
format: Schema.optional(Schema.String),
|
||||
providerOptions: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
images: Schema.optional(Schema.Array(ImageInputSchema)),
|
||||
options: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
http: Schema.optional(HttpOptions),
|
||||
}) {
|
||||
declare protected readonly _ImageRequest: void
|
||||
}
|
||||
|
||||
export type ImageRequestFor<Options extends ImageOptions = ImageOptions> = Omit<
|
||||
ImageRequest,
|
||||
"model" | "providerOptions"
|
||||
> & {
|
||||
export type ImageRequestFor<Options extends ImageOptions = ImageOptions> = Omit<ImageRequest, "model" | "options"> & {
|
||||
readonly model: ImageModel<Options>
|
||||
readonly providerOptions?: Options
|
||||
readonly options?: Options
|
||||
}
|
||||
|
||||
export type ImageModelOptions<Model> = Model extends ImageModel<infer Options> ? Options : never
|
||||
|
||||
export type ImageRequestInput<Model extends ImageModel = ImageModel> = Omit<
|
||||
export type ImageRequestInput<Model extends object = ImageModel> = Omit<
|
||||
ConstructorParameters<typeof ImageRequest>[0],
|
||||
"model" | "providerOptions" | "http"
|
||||
"model" | "options" | "http"
|
||||
> & {
|
||||
readonly model: Model
|
||||
readonly format?: ImageFormat
|
||||
readonly providerOptions?: NoInfer<ImageModelOptions<Model>>
|
||||
readonly options?: NoInfer<ImageModelOptions<Model>>
|
||||
readonly http?: HttpOptions.Input
|
||||
}
|
||||
} & (Model extends ImageModel<ImageModelOptions<Model>> ? unknown : never)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Response and events
|
||||
// ---------------------------------------------------------------------------
|
||||
export class GeneratedImage extends Schema.Class<GeneratedImage>("Image.Generated")({
|
||||
mediaType: Schema.String,
|
||||
data: Schema.Union([Schema.String, Schema.Uint8Array]),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}) {}
|
||||
|
||||
export class ImageResponse extends Schema.Class<ImageResponse>("Image.Response")({
|
||||
images: Schema.Array(Media.AssetSchema),
|
||||
usage: Schema.optional(MediaUsage),
|
||||
notices: Schema.optional(Schema.Array(Media.Notice)),
|
||||
images: Schema.Array(GeneratedImage),
|
||||
usage: Schema.optional(Usage),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}) {
|
||||
get image() {
|
||||
@@ -115,63 +136,7 @@ export class ImageResponse extends Schema.Class<ImageResponse>("Image.Response")
|
||||
}
|
||||
}
|
||||
|
||||
/** The `index`-th lower-fidelity preview of an image still generating; the `image` event supersedes it. */
|
||||
export const ImagePartialEvent = Schema.Struct({
|
||||
type: Schema.tag("image-partial"),
|
||||
index: Schema.Number,
|
||||
image: Media.AssetSchema,
|
||||
}).annotate({ identifier: "Image.Event.Partial" })
|
||||
|
||||
export const ImageOutputEvent = Schema.Struct({
|
||||
type: Schema.tag("image"),
|
||||
index: Schema.Number,
|
||||
image: Media.AssetSchema,
|
||||
}).annotate({ identifier: "Image.Event.Image" })
|
||||
|
||||
export const ImageFinishEvent = Schema.Struct({
|
||||
type: Schema.tag("finish"),
|
||||
usage: Schema.optional(MediaUsage),
|
||||
notices: Schema.optional(Schema.Array(Media.Notice)),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "Image.Event.Finish" })
|
||||
|
||||
const imageEventTagged = Schema.Union([
|
||||
QueuedEvent,
|
||||
ProgressEvent,
|
||||
ImagePartialEvent,
|
||||
ImageOutputEvent,
|
||||
ImageFinishEvent,
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export const ImageEvent = Object.assign(imageEventTagged, {
|
||||
is: {
|
||||
generationQueued: imageEventTagged.guards["generation-queued"],
|
||||
generationProgress: imageEventTagged.guards["generation-progress"],
|
||||
imagePartial: imageEventTagged.guards["image-partial"],
|
||||
image: imageEventTagged.guards.image,
|
||||
finish: imageEventTagged.guards.finish,
|
||||
},
|
||||
})
|
||||
export type ImageEvent = Schema.Schema.Type<typeof imageEventTagged>
|
||||
|
||||
const collectResponse = (events: ReadonlyArray<ImageEvent>): Effect.Effect<ImageResponse> => {
|
||||
const finish = events.find(ImageEvent.is.finish)
|
||||
// Every image protocol's `finish` emits the terminal event or fails, so a completed stream always has one.
|
||||
if (finish === undefined) return Effect.die(new Error("The image stream completed without a finish event"))
|
||||
return Effect.succeed(
|
||||
new ImageResponse({
|
||||
images: events.filter(ImageEvent.is.image).map((event) => event.image),
|
||||
usage: finish.usage,
|
||||
notices: finish.notices,
|
||||
providerMetadata: finish.providerMetadata,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Request-shaped call API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function request<const Model extends ImageModel>(
|
||||
export function request<const Model extends object>(
|
||||
input: ImageRequestInput<Model>,
|
||||
): ImageRequestFor<ImageModelOptions<Model>>
|
||||
export function request(input: ImageRequest): ImageRequest
|
||||
@@ -179,43 +144,28 @@ export function request(input: ImageRequest | ImageRequestInput) {
|
||||
if (input instanceof ImageRequest) return input
|
||||
return new ImageRequest({
|
||||
...input,
|
||||
http: HttpOptions.make(input.http),
|
||||
model: input.model as unknown as ImageModel,
|
||||
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
|
||||
})
|
||||
}
|
||||
|
||||
const requestEffect = (input: ImageRequest | ImageRequestInput) => tryRequest(() => request(input))
|
||||
|
||||
export function generate<const Model extends ImageModel>(
|
||||
input: ImageRequest | ImageRequestInput<Model>,
|
||||
options?: AwaitOptions,
|
||||
export function generate<const Model extends object>(
|
||||
input: ImageRequestInput<Model>,
|
||||
): Effect.Effect<ImageResponse, AIError, Service>
|
||||
export function generate(input: ImageRequest | ImageRequestInput, options?: AwaitOptions) {
|
||||
return requestEffect(input).pipe(Effect.flatMap((request) => ImageClient.generate(request, options)))
|
||||
export function generate(input: ImageRequest): Effect.Effect<ImageResponse, AIError, Service>
|
||||
export function generate(input: ImageRequest | ImageRequestInput) {
|
||||
return Effect.try({
|
||||
try: () => (input instanceof ImageRequest ? input : request(input)),
|
||||
catch: (error) =>
|
||||
new AIError({
|
||||
module: "Image",
|
||||
method: "generate",
|
||||
reason: new InvalidRequestReason({ message: error instanceof Error ? error.message : String(error) }),
|
||||
}),
|
||||
}).pipe(Effect.flatMap((request) => ImageClient.generate(request as unknown as ImageRequestFor<ImageOptions>)))
|
||||
}
|
||||
|
||||
export function stream<const Model extends ImageModel>(
|
||||
input: ImageRequest | ImageRequestInput<Model>,
|
||||
options?: AwaitOptions,
|
||||
): Stream.Stream<ImageEvent, AIError, Service>
|
||||
export function stream(input: ImageRequest | ImageRequestInput, options?: AwaitOptions) {
|
||||
return Stream.unwrap(requestEffect(input).pipe(Effect.map((request) => ImageClient.stream(request, options))))
|
||||
}
|
||||
|
||||
/** Inline and streaming routes fail with `UnsupportedOperation`. */
|
||||
export function start<const Model extends ImageModel>(
|
||||
input: ImageRequest | ImageRequestInput<Model>,
|
||||
): Effect.Effect<Generation<ImageResponse>, AIError, Service>
|
||||
export function start(input: ImageRequest | ImageRequestInput) {
|
||||
return requestEffect(input).pipe(Effect.flatMap((request) => ImageClient.start(request)))
|
||||
}
|
||||
|
||||
export const resume = (model: ImageModel, token: unknown): Effect.Effect<Generation<ImageResponse>, AIError, Service> =>
|
||||
ImageClient.resume(model, token)
|
||||
|
||||
export const Image = {
|
||||
request,
|
||||
generate,
|
||||
stream,
|
||||
start,
|
||||
resume,
|
||||
} as const
|
||||
@@ -1,102 +1,19 @@
|
||||
export { AIClient } from "./ai-client.js"
|
||||
export { LLMClient } from "./route/client.js"
|
||||
export { ImageClient } from "./image-client.js"
|
||||
export { Auth } from "./route/auth.js"
|
||||
export { Provider } from "./provider.js"
|
||||
export { ProviderPackage } from "./provider-package.js"
|
||||
export { isContextOverflow, isContextOverflowFailure, isRetryable } from "./provider-error.js"
|
||||
export { isContextOverflow, isContextOverflowFailure } from "./provider-error.js"
|
||||
export type {
|
||||
RouteLanguageModelInput,
|
||||
RouteRoutedLanguageModelInput,
|
||||
Interface as LLMClientShape,
|
||||
LLMClientService,
|
||||
Service as LLMClientService,
|
||||
} from "./route/client.js"
|
||||
export * from "./schema/index.js"
|
||||
export {
|
||||
ImageAspectRatio,
|
||||
ImageEvent,
|
||||
ImageModel,
|
||||
ImageModelSchema,
|
||||
ImageRequest,
|
||||
ImageResponse,
|
||||
ImageSize,
|
||||
} from "./image.js"
|
||||
export type {
|
||||
ImageFormat,
|
||||
ImageModelOptions,
|
||||
ImageOptions,
|
||||
ImageRequestFor,
|
||||
ImageRequestInput,
|
||||
ImageRoute,
|
||||
} from "./image.js"
|
||||
export { GeneratedImage, ImageInput, ImageInputSchema, ImageModel, ImageRequest, ImageResponse } from "./image.js"
|
||||
export type { ImageModelOptions, ImageOptions, ImageRequestFor, ImageRequestInput, ImageRoute } from "./image.js"
|
||||
export { Image } from "./image.js"
|
||||
export { VideoClient } from "./video-client.js"
|
||||
export {
|
||||
VideoAspectRatio,
|
||||
VideoEvent,
|
||||
VideoFrames,
|
||||
VideoModel,
|
||||
VideoModelSchema,
|
||||
VideoRequest,
|
||||
VideoResponse,
|
||||
} from "./video.js"
|
||||
export type {
|
||||
VideoModelOptions,
|
||||
VideoOptions,
|
||||
VideoRequestFor,
|
||||
VideoRequestInput,
|
||||
VideoResolution,
|
||||
VideoRoute,
|
||||
} from "./video.js"
|
||||
export { Video } from "./video.js"
|
||||
export { SpeechClient } from "./speech-client.js"
|
||||
export {
|
||||
SpeechEvent,
|
||||
SpeechModel,
|
||||
SpeechModelSchema,
|
||||
SpeechRequest,
|
||||
SpeechResponse,
|
||||
SpeechTimestamp,
|
||||
SpeechVoice,
|
||||
} from "./speech.js"
|
||||
export type {
|
||||
SpeechFormat,
|
||||
SpeechModelOptions,
|
||||
SpeechOptions,
|
||||
SpeechRequestFor,
|
||||
SpeechRequestInput,
|
||||
SpeechRoute,
|
||||
} from "./speech.js"
|
||||
export { Speech } from "./speech.js"
|
||||
export { TranscriptionClient } from "./transcription-client.js"
|
||||
export {
|
||||
TranscriptionEvent,
|
||||
TranscriptionModel,
|
||||
TranscriptionModelSchema,
|
||||
TranscriptionRequest,
|
||||
TranscriptionResponse,
|
||||
TranscriptionSegment,
|
||||
TranscriptionTimestamps,
|
||||
TranscriptionWord,
|
||||
} from "./transcription.js"
|
||||
export type {
|
||||
TranscriptionModelOptions,
|
||||
TranscriptionOptions,
|
||||
TranscriptionRequestFor,
|
||||
TranscriptionRequestInput,
|
||||
TranscriptionRoute,
|
||||
} from "./transcription.js"
|
||||
export { Transcription } from "./transcription.js"
|
||||
export { Media } from "./media.js"
|
||||
export { Generation } from "./generation.js"
|
||||
export type {
|
||||
AwaitOptions as GenerationAwaitOptions,
|
||||
Event as GenerationEvent,
|
||||
Poll,
|
||||
Route as GenerationRoute,
|
||||
Snapshot as GenerationSnapshot,
|
||||
Status as GenerationStatus,
|
||||
} from "./generation.js"
|
||||
export { Tool, ToolFailure, toDefinitions } from "./tool.js"
|
||||
export { ToolRuntime } from "./tool-runtime.js"
|
||||
export type { DispatchResult as ToolDispatchResult, ToolSettlement } from "./tool-runtime.js"
|
||||
|
||||
+15
-31
@@ -1,10 +1,9 @@
|
||||
import { Effect, JsonSchema, Schema, Stream } from "effect"
|
||||
import { tryRequest } from "./media-model.js"
|
||||
import { LLMClient, Service, type StreamOptions } from "./route/client.js"
|
||||
import { Effect, JsonSchema, Schema } from "effect"
|
||||
import { LLMClient, Service } from "./route/client.js"
|
||||
import {
|
||||
GenerationOptions,
|
||||
HttpOptions,
|
||||
InvalidProviderOutputError,
|
||||
InvalidProviderOutputReason,
|
||||
AIError,
|
||||
LLMEvent,
|
||||
LLMRequest,
|
||||
@@ -13,10 +12,9 @@ import {
|
||||
LanguageModel,
|
||||
SystemPart,
|
||||
ToolChoice,
|
||||
ToolEntry,
|
||||
ToolDefinition,
|
||||
type ContentPart,
|
||||
type LanguageModelProviderOptions,
|
||||
type ToolEntryInput,
|
||||
} from "./schema/index.js"
|
||||
import { make as makeTool, toDefinitions, type ToolSchema } from "./tool.js"
|
||||
|
||||
@@ -29,33 +27,16 @@ export type RequestInput<SelectedLanguageModel extends LanguageModel = LanguageM
|
||||
readonly system?: string | SystemPart | ReadonlyArray<SystemPart>
|
||||
readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart>
|
||||
readonly messages?: ReadonlyArray<Message | Message.Input>
|
||||
readonly tools?: ReadonlyArray<ToolEntryInput>
|
||||
readonly tools?: ReadonlyArray<ToolDefinition.Input>
|
||||
readonly toolChoice?: ToolChoice.Input
|
||||
readonly generation?: GenerationOptions.Input
|
||||
readonly providerOptions?: NoInfer<LanguageModelProviderOptions<SelectedLanguageModel>>
|
||||
readonly http?: HttpOptions.Input
|
||||
}
|
||||
|
||||
export function generate<const Model extends LanguageModel>(
|
||||
input: RequestInput<Model>,
|
||||
options?: StreamOptions,
|
||||
): Effect.Effect<LLMResponse, AIError, Service>
|
||||
export function generate(input: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, AIError, Service>
|
||||
export function generate(input: RequestInput | LLMRequest, options?: StreamOptions) {
|
||||
return requestEffect(input).pipe(Effect.flatMap((request) => LLMClient.generate(request, options)))
|
||||
}
|
||||
export const generate = LLMClient.generate
|
||||
|
||||
export function stream<const Model extends LanguageModel>(
|
||||
input: RequestInput<Model>,
|
||||
options?: StreamOptions,
|
||||
): Stream.Stream<LLMEvent, AIError, Service>
|
||||
export function stream(input: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, AIError, Service>
|
||||
export function stream(input: RequestInput | LLMRequest, options?: StreamOptions) {
|
||||
return Stream.unwrap(requestEffect(input).pipe(Effect.map((request) => LLMClient.stream(request, options))))
|
||||
}
|
||||
|
||||
const requestEffect = (input: RequestInput | LLMRequest) =>
|
||||
input instanceof LLMRequest ? Effect.succeed(input) : tryRequest(() => request(input))
|
||||
export const stream = LLMClient.stream
|
||||
|
||||
export const request = <const SelectedLanguageModel extends LanguageModel>(
|
||||
input: RequestInput<SelectedLanguageModel>,
|
||||
@@ -75,11 +56,11 @@ export const request = <const SelectedLanguageModel extends LanguageModel>(
|
||||
...rest,
|
||||
system: SystemPart.content(requestSystem),
|
||||
messages: [...(messages?.map(Message.make) ?? []), ...(prompt === undefined ? [] : [Message.user(prompt)])],
|
||||
tools: tools?.map(ToolEntry.make) ?? [],
|
||||
tools: tools?.map(ToolDefinition.make) ?? [],
|
||||
toolChoice: requestToolChoice ? ToolChoice.make(requestToolChoice) : undefined,
|
||||
generation: requestGeneration === undefined ? undefined : GenerationOptions.make(requestGeneration),
|
||||
providerOptions: requestProviderOptions,
|
||||
http: HttpOptions.make(requestHttp),
|
||||
http: requestHttp === undefined ? undefined : HttpOptions.make(requestHttp),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,7 +116,9 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
||||
)
|
||||
if (!call || !LLMEvent.is.toolCall(call))
|
||||
return yield* new AIError({
|
||||
reason: new InvalidProviderOutputError({
|
||||
module: "LLM",
|
||||
method: "generateObject",
|
||||
reason: new InvalidProviderOutputReason({
|
||||
message: `generateObject: model did not call the forced \`${GENERATE_OBJECT_TOOL_NAME}\` tool`,
|
||||
}),
|
||||
})
|
||||
@@ -143,9 +126,10 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new AIError({
|
||||
reason: new InvalidProviderOutputError({
|
||||
module: "LLM",
|
||||
method: "generateObject",
|
||||
reason: new InvalidProviderOutputReason({
|
||||
message: `generateObject: tool input failed schema decode: ${error.message}`,
|
||||
cause: error,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { type Context, Effect, Layer, Stream } from "effect"
|
||||
import { resultEvents, type AwaitOptions, type Generation, type Observation } from "./generation.js"
|
||||
import { RequestExecutor } from "./route/executor.js"
|
||||
import type { MediaRoute } from "./route/media.js"
|
||||
import { AIError, UnsupportedOperationError } from "./schema/index.js"
|
||||
|
||||
/** A media request whose model carries the route that executes it. */
|
||||
export interface RoutedRequest<Self extends MediaRoute.MediaRequest, Event, Response> extends MediaRoute.MediaRequest {
|
||||
readonly model: MediaRoute.MediaRequest["model"] & { readonly route: MediaRoute.AnyRoute<Self, Event, Response> }
|
||||
}
|
||||
|
||||
/** `start` and `resume` fail with `UnsupportedOperation` on inline and stream routes. */
|
||||
export interface Interface<Req extends RoutedRequest<Req, Event, Response>, Event, Response> {
|
||||
readonly generate: (request: Req, options?: AwaitOptions) => Effect.Effect<Response, AIError>
|
||||
readonly stream: (request: Req, options?: AwaitOptions) => Stream.Stream<Event | Observation, AIError>
|
||||
readonly start: (request: Req) => Effect.Effect<Generation<Response>, AIError>
|
||||
readonly resume: (model: Req["model"], token: unknown) => Effect.Effect<Generation<Response>, AIError>
|
||||
}
|
||||
|
||||
/** One modality's layer and service accessors, dispatching each request on its route's `kind`. */
|
||||
export const make = <Self, Req extends RoutedRequest<Req, Event, Response>, Event, Response>(
|
||||
service: Context.Service<Self, Interface<Req, Event, Response>>,
|
||||
input: {
|
||||
readonly modality: string
|
||||
/** A completed response expanded into the streaming event shape. */
|
||||
readonly responseEvents: (response: Response) => ReadonlyArray<Event>
|
||||
},
|
||||
) => ({
|
||||
layer: Layer.effect(
|
||||
service,
|
||||
Effect.gen(function* () {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const notQueued = (route: MediaRoute.AnyRoute<Req, Event, Response>, operation: string) =>
|
||||
new AIError({
|
||||
reason: new UnsupportedOperationError({
|
||||
operation: `${input.modality}.${operation}`,
|
||||
provider: route.provider,
|
||||
route: route.id,
|
||||
message: `${route.provider}/${route.id} is not a queued route; use generate or stream`,
|
||||
}),
|
||||
})
|
||||
const start = (request: Req) => {
|
||||
const route = request.model.route
|
||||
if (route.kind !== "queued") return Effect.fail(notQueued(route, "start"))
|
||||
return route.start(request, executor.execute)
|
||||
}
|
||||
return service.of({
|
||||
start,
|
||||
resume: (model, token) => {
|
||||
if (model.route.kind !== "queued") return Effect.fail(notQueued(model.route, "resume"))
|
||||
return model.route.resume(model, token, executor.execute)
|
||||
},
|
||||
generate: (request, options) => {
|
||||
const route = request.model.route
|
||||
if (route.kind !== "queued") return route.generate(request, executor.execute)
|
||||
return start(request).pipe(Effect.flatMap((generation) => generation.await(options)))
|
||||
},
|
||||
stream: (request, options) => {
|
||||
const route = request.model.route
|
||||
if (route.kind === "stream") return route.stream(request, executor.execute)
|
||||
if (route.kind === "queued")
|
||||
return Stream.unwrap(
|
||||
start(request).pipe(Effect.map((generation) => resultEvents(generation, input.responseEvents, options))),
|
||||
)
|
||||
return Stream.fromIterableEffect(Effect.map(route.generate(request, executor.execute), input.responseEvents))
|
||||
},
|
||||
})
|
||||
}),
|
||||
),
|
||||
generate: (request: Req, options?: AwaitOptions) => service.use((client) => client.generate(request, options)),
|
||||
stream: (request: Req, options?: AwaitOptions) =>
|
||||
Stream.unwrap(service.useSync((client) => client.stream(request, options))),
|
||||
start: (request: Req) => service.use((client) => client.start(request)),
|
||||
resume: (model: Req["model"], token: unknown) => service.use((client) => client.resume(model, token)),
|
||||
})
|
||||
|
||||
export * as MediaClient from "./media-client.js"
|
||||
@@ -1,110 +0,0 @@
|
||||
import { Effect } from "effect"
|
||||
import { Endpoint } from "./route/endpoint.js"
|
||||
import { MediaRoute } from "./route/media.js"
|
||||
import type { MediaProtocol } from "./route/media-protocol.js"
|
||||
import { AIError, HttpOptions, InvalidRequestError, ModelID, ProviderID } from "./schema/index.js"
|
||||
|
||||
/**
|
||||
* What every media model carries: ids, the configured route, and deployment `http` overlays. Modality classes
|
||||
* (`ImageModel`, `VideoModel`, `SpeechModel`, `TranscriptionModel`) extend it with their route type and a nominal
|
||||
* marker so one cannot stand in for the other in requests.
|
||||
*/
|
||||
export class MediaModel<Route, Options> {
|
||||
// As with `LanguageModel`, the route type is erased over `Options`; `fromRoute` and the constructor trust that the
|
||||
// route accepts every request this model's `Options` admit.
|
||||
declare protected readonly _Options: Options
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: Route
|
||||
readonly http?: HttpOptions
|
||||
|
||||
constructor(input: MediaModel.Input<Route>) {
|
||||
this.id = ModelID.make(input.id)
|
||||
this.provider = ProviderID.make(input.provider)
|
||||
this.route = input.route
|
||||
this.http = input.http
|
||||
}
|
||||
}
|
||||
|
||||
export namespace MediaModel {
|
||||
export type Options = Record<string, unknown>
|
||||
|
||||
export interface Input<Route> {
|
||||
readonly id: string | ModelID
|
||||
readonly provider: string | ProviderID
|
||||
readonly route: Route
|
||||
readonly http?: HttpOptions
|
||||
}
|
||||
|
||||
/** A protocol plus its canonical start path; `ModelInput.baseURL` overrides `baseURL` per deployment. */
|
||||
export interface RouteInput<Request extends MediaRoute.MediaRequest, Protocol> {
|
||||
readonly protocol: Protocol
|
||||
readonly path: Endpoint.EndpointPart<MediaProtocol.Body, Request>
|
||||
readonly baseURL?: string
|
||||
/** Headers the protocol requires on every call, such as a pinned API version; deployment headers win. */
|
||||
readonly headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export type InlineRouteInput<Request extends MediaRoute.MediaRequest, Response> = RouteInput<
|
||||
Request,
|
||||
MediaProtocol.Inline<Request, Response>
|
||||
>
|
||||
|
||||
export type StreamRouteInput<Request extends MediaRoute.MediaRequest, Event, Frame, State> = RouteInput<
|
||||
MediaProtocol.Addressed<Request>,
|
||||
MediaProtocol.Streamed<Request, Event, Frame, State>
|
||||
>
|
||||
|
||||
export type QueuedRouteInput<Request extends MediaRoute.MediaRequest, Response, Token> = RouteInput<
|
||||
Request,
|
||||
MediaProtocol.Queued<Request, Response, Token>
|
||||
>
|
||||
|
||||
export type AnyRouteInput<Request extends MediaRoute.MediaRequest, Event, Response, Frame, State, Token> =
|
||||
| InlineRouteInput<Request, Response>
|
||||
| StreamRouteInput<Request, Event, Frame, State>
|
||||
| QueuedRouteInput<Request, Response, Token>
|
||||
}
|
||||
|
||||
/** Compose a protocol route input with one deployment through `MediaRoute.inline`, `queued`, or `stream`. */
|
||||
export const composeRoute = <Request extends MediaRoute.MediaRequest, Event, Response, Frame, State, Token>(
|
||||
route: MediaModel.AnyRouteInput<Request, Event, Response, Frame, State, Token>,
|
||||
input: MediaRoute.ModelInput,
|
||||
collect: (events: ReadonlyArray<Event>) => Effect.Effect<Response, AIError>,
|
||||
): MediaRoute.AnyRoute<Request, Event, Response> => {
|
||||
if (isStreamInput(route)) return MediaRoute.stream({ ...composition(route, input), collect })
|
||||
if (isQueuedInput(route)) return MediaRoute.queued(composition(route, input))
|
||||
return MediaRoute.inline(composition(route, input))
|
||||
}
|
||||
|
||||
const composition = <Request extends MediaRoute.MediaRequest, Protocol>(
|
||||
route: MediaModel.RouteInput<Request, Protocol>,
|
||||
input: MediaRoute.ModelInput,
|
||||
): MediaRoute.Composition<Request> & { readonly protocol: Protocol } => ({
|
||||
protocol: route.protocol,
|
||||
endpoint: Endpoint.path(route.path, { baseURL: input.baseURL ?? route.baseURL }),
|
||||
auth: input.auth,
|
||||
headers:
|
||||
route.headers === undefined && input.headers === undefined ? undefined : { ...route.headers, ...input.headers },
|
||||
})
|
||||
|
||||
const isStreamInput = <Request extends MediaRoute.MediaRequest, Event, Response, Frame, State, Token>(
|
||||
route: MediaModel.AnyRouteInput<Request, Event, Response, Frame, State, Token>,
|
||||
): route is MediaModel.StreamRouteInput<Request, Event, Frame, State> => route.protocol.kind === "stream"
|
||||
|
||||
const isQueuedInput = <Request extends MediaRoute.MediaRequest, Event, Response, Frame, State, Token>(
|
||||
route: MediaModel.AnyRouteInput<Request, Event, Response, Frame, State, Token>,
|
||||
): route is MediaModel.QueuedRouteInput<Request, Response, Token> => route.protocol.kind === "queued"
|
||||
|
||||
/** Lift a synchronous Schema-class constructor into a typed `InvalidRequest` failure. */
|
||||
export const tryRequest = <A>(make: () => A): Effect.Effect<A, AIError> =>
|
||||
Effect.try({
|
||||
try: make,
|
||||
catch: (error) =>
|
||||
new AIError({
|
||||
reason: new InvalidRequestError({
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
cause: error,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
@@ -1,324 +0,0 @@
|
||||
export * as Media from "./media.js"
|
||||
|
||||
import { Effect, FileSystem, Schema, SchemaGetter } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { ProviderID } from "./schema/ids.js"
|
||||
import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js"
|
||||
import { ProviderMetadata } from "./schema/options.js"
|
||||
import { Service } from "./route/executor-service.js"
|
||||
import { detectMediaType, fileMediaType } from "./utils/media-type.js"
|
||||
|
||||
export { detectMediaType } from "./utils/media-type.js"
|
||||
|
||||
const OCTET_STREAM = "application/octet-stream"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Source — the serializable wire/persistence form of a media asset
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BytesSource = Schema.Struct({
|
||||
type: Schema.Literal("bytes"),
|
||||
data: Schema.Uint8Array,
|
||||
mediaType: Schema.String,
|
||||
})
|
||||
|
||||
const Base64Source = Schema.Struct({
|
||||
type: Schema.Literal("base64"),
|
||||
data: Schema.String,
|
||||
mediaType: Schema.String,
|
||||
})
|
||||
|
||||
const UrlSource = Schema.Struct({
|
||||
type: Schema.Literal("url"),
|
||||
url: Schema.String,
|
||||
mediaType: Schema.optional(Schema.String),
|
||||
/** Epoch milliseconds after which the provider no longer serves the URL. */
|
||||
expiresAt: Schema.optional(Schema.Number),
|
||||
})
|
||||
|
||||
/** A provider-side handle: OpenAI `file_id`, Gemini file URI, `gs://`, `runway://`, or a prior generation id. */
|
||||
const RefSource = Schema.Struct({
|
||||
type: Schema.Literal("ref"),
|
||||
provider: ProviderID,
|
||||
id: Schema.String,
|
||||
mediaType: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
export const Source = Schema.Union([BytesSource, Base64Source, UrlSource, RefSource])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({ identifier: "Media.Source" })
|
||||
export type Source = Schema.Schema.Type<typeof Source>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kind, Info, Notice
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type AspectRatio = `${number}:${number}`
|
||||
export const AspectRatio = Schema.declare<AspectRatio>(
|
||||
(value): value is AspectRatio => typeof value === "string" && /^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/.test(value),
|
||||
{ title: "Media.AspectRatio" },
|
||||
)
|
||||
|
||||
export const Kind = Schema.Literals(["image", "video", "audio", "document", "other"])
|
||||
export type Kind = Schema.Schema.Type<typeof Kind>
|
||||
|
||||
export const kindOf = (mediaType: string): Kind => {
|
||||
const lower = mediaType.toLowerCase()
|
||||
if (lower.startsWith("image/")) return "image"
|
||||
if (lower.startsWith("video/")) return "video"
|
||||
if (lower.startsWith("audio/")) return "audio"
|
||||
if (lower === "application/pdf" || lower.startsWith("text/")) return "document"
|
||||
return "other"
|
||||
}
|
||||
|
||||
/** Container-independent facts about the payload; raw PCM audio relies on these because it has no header. */
|
||||
export const Info = Schema.Struct({
|
||||
width: Schema.optional(Schema.Number),
|
||||
height: Schema.optional(Schema.Number),
|
||||
durationSeconds: Schema.optional(Schema.Number),
|
||||
sampleRate: Schema.optional(Schema.Number),
|
||||
channels: Schema.optional(Schema.Number),
|
||||
encoding: Schema.optional(Schema.String),
|
||||
format: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "Media.Info" })
|
||||
export type Info = Schema.Schema.Type<typeof Info>
|
||||
|
||||
/** A provider-side partial result such as stripped audio or a moderated sample; never a silent drop. */
|
||||
export const Notice = Schema.Struct({
|
||||
type: Schema.Literals(["moderated", "filtered", "other"]),
|
||||
message: Schema.String,
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "Media.Notice" })
|
||||
export type Notice = Schema.Schema.Type<typeof Notice>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Asset
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const invalid = (message: string, cause?: unknown) =>
|
||||
new AIError({ reason: new InvalidRequestError({ message, cause }) })
|
||||
|
||||
/** Synchronous view of an inline payload; `undefined` for `url` and `ref` sources, which carry no local bytes. */
|
||||
export interface Inline {
|
||||
readonly mime: string
|
||||
readonly base64: string
|
||||
readonly dataUrl: string
|
||||
}
|
||||
|
||||
export class Asset {
|
||||
readonly source: Source
|
||||
/** Derived from the source: declared type, sniffed magic bytes, then `application/octet-stream`. */
|
||||
readonly mediaType: string
|
||||
readonly kind: Kind
|
||||
readonly info?: Info
|
||||
/** Epoch milliseconds after which a `url` source stops resolving. */
|
||||
readonly expiresAt?: number
|
||||
readonly providerMetadata?: ProviderMetadata
|
||||
/** Transient download credentials for `url` sources; see `Asset.Input.headers`. */
|
||||
readonly headers?: Record<string, string>
|
||||
|
||||
// Derived payload forms are cached on the instance because every protocol lowering re-reads the same payload. The
|
||||
// cache is check-then-set (concurrent first reads of a `url` source may both download) and is never observable
|
||||
// through `source`, so round-tripping through `Media.from(asset.source)` stays lossless.
|
||||
#bytes: Uint8Array | undefined
|
||||
#base64: string | undefined
|
||||
|
||||
constructor(input: Asset.Input) {
|
||||
this.source = input.source
|
||||
this.mediaType =
|
||||
input.source.mediaType ??
|
||||
(input.source.type === "bytes" ? detectMediaType(input.source.data) : undefined) ??
|
||||
OCTET_STREAM
|
||||
this.kind = kindOf(this.mediaType)
|
||||
this.info = input.info
|
||||
this.expiresAt = input.source.type === "url" ? input.source.expiresAt : undefined
|
||||
this.providerMetadata = input.providerMetadata
|
||||
this.headers = input.source.type === "url" ? input.headers : undefined
|
||||
}
|
||||
|
||||
/** Inline payload without effects, for protocols that embed base64 or data URLs directly. */
|
||||
inline(): Inline | undefined {
|
||||
const source = this.source
|
||||
if (source.type !== "bytes" && source.type !== "base64") return undefined
|
||||
const base64 = source.type === "base64" ? source.data : (this.#base64 ??= Base64.encode(source.data))
|
||||
const mime = this.mediaType.toLowerCase()
|
||||
return { mime, base64, dataUrl: `data:${mime};base64,${base64}` }
|
||||
}
|
||||
|
||||
/** Decoded payload; downloads `url` sources through the request executor and caches the result. */
|
||||
bytes(): Effect.Effect<Uint8Array, AIError, Service> {
|
||||
return Effect.suspend(() => {
|
||||
const source = this.source
|
||||
if (source.type === "bytes") return Effect.succeed(source.data)
|
||||
if (this.#bytes !== undefined) return Effect.succeed(this.#bytes)
|
||||
if (source.type === "ref")
|
||||
return Effect.fail(invalid(`Cannot materialize provider ref ${source.provider}:${source.id}`))
|
||||
const decoded =
|
||||
source.type === "base64"
|
||||
? Effect.fromResult(Base64.decode(source.data)).pipe(
|
||||
Effect.mapError((cause) => invalid(`Media asset contains invalid base64 data`, cause)),
|
||||
)
|
||||
: download(source, this.headers)
|
||||
return decoded.pipe(Effect.tap((data) => Effect.sync(() => (this.#bytes = data))))
|
||||
})
|
||||
}
|
||||
|
||||
base64(): Effect.Effect<string, AIError, Service> {
|
||||
return Effect.suspend(() => {
|
||||
const source = this.source
|
||||
if (source.type === "base64") return Effect.succeed(source.data)
|
||||
if (this.#base64 !== undefined) return Effect.succeed(this.#base64)
|
||||
return this.bytes().pipe(Effect.map((data) => (this.#base64 = Base64.encode(data))))
|
||||
})
|
||||
}
|
||||
|
||||
dataUrl(): Effect.Effect<string, AIError, Service> {
|
||||
return this.base64().pipe(Effect.map((data) => `data:${this.mediaType};base64,${data}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* The `AssetEncoded` JSON form with `bytes` sources as base64, matching `Schema.toCodecJson(AssetSchema)`, so a
|
||||
* plain `JSON.stringify` of messages or events stays lossless and decodes back through the JSON codec.
|
||||
*/
|
||||
toJSON() {
|
||||
const source = this.source
|
||||
return {
|
||||
source: source.type === "bytes" ? { ...source, data: Base64.encode(source.data) } : source,
|
||||
info: this.info,
|
||||
providerMetadata: this.providerMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
/** Pull `url` sources into owned bytes before the URL expires. Inline sources return themselves. */
|
||||
materialize(): Effect.Effect<Asset, AIError, Service> {
|
||||
if (this.source.type === "bytes" || this.source.type === "base64") return Effect.succeed(this)
|
||||
return this.bytes().pipe(
|
||||
Effect.map((data) =>
|
||||
bytes(data, this.source.mediaType, { info: this.info, providerMetadata: this.providerMetadata }),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Asset {
|
||||
export interface Input {
|
||||
readonly source: Source
|
||||
readonly info?: Info
|
||||
readonly providerMetadata?: ProviderMetadata
|
||||
/**
|
||||
* Headers required to download a `url` source, such as the provider API key Veo demands for its file URIs.
|
||||
* They are runtime-only: never part of `source`, `toJSON()`, or `AssetSchema`, so a persisted asset cannot leak
|
||||
* credentials and cannot be downloaded again after a round-trip. Call `materialize()` before persisting.
|
||||
*/
|
||||
readonly headers?: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
/** JSON form of an asset: the serializable `Source` plus caller-supplied metadata. `bytes` sources encode as base64. */
|
||||
export const AssetEncoded = Schema.Struct({
|
||||
source: Source,
|
||||
info: Schema.optional(Info),
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}).annotate({ identifier: "Media.AssetEncoded" })
|
||||
|
||||
const encodeAsset = (asset: Asset): typeof AssetEncoded.Type => ({
|
||||
source: asset.source,
|
||||
info: asset.info,
|
||||
providerMetadata: asset.providerMetadata,
|
||||
})
|
||||
|
||||
const AssetInstance = Schema.declare((value): value is Asset => value instanceof Asset, {
|
||||
expected: "Media.Asset",
|
||||
})
|
||||
|
||||
/** `Asset` in the type domain and `AssetEncoded` on the wire, so messages and events holding assets serialize. */
|
||||
export const AssetSchema = AssetEncoded.pipe(
|
||||
Schema.decodeTo(AssetInstance, {
|
||||
decode: SchemaGetter.transform((encoded) => new Asset(encoded)),
|
||||
encode: SchemaGetter.transform(encodeAsset),
|
||||
}),
|
||||
)
|
||||
|
||||
const download = Effect.fn("Media.download")(function* (
|
||||
source: Extract<Source, { readonly type: "url" }>,
|
||||
headers: Record<string, string> | undefined,
|
||||
) {
|
||||
const executor = yield* Service
|
||||
const response = yield* executor.execute(
|
||||
HttpClientRequest.get(source.url).pipe(HttpClientRequest.setHeaders(headers ?? {})),
|
||||
)
|
||||
const buffer = yield* response.arrayBuffer.pipe(
|
||||
Effect.mapError(
|
||||
(cause) =>
|
||||
new AIError({
|
||||
reason: new InvalidProviderOutputError({
|
||||
message: `Failed to read media from ${source.url}`,
|
||||
http: new HttpContext({ url: response.request.url, status: response.status, headers: response.headers }),
|
||||
cause,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
return new Uint8Array(buffer)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type AssetOptions = Omit<Asset.Input, "source">
|
||||
|
||||
export const from = (source: Source, options?: AssetOptions) => new Asset({ ...options, source })
|
||||
|
||||
export const bytes = (data: Uint8Array, mediaType?: string, options?: AssetOptions) =>
|
||||
from({ type: "bytes", data, mediaType: mediaType ?? detectMediaType(data) ?? OCTET_STREAM }, options)
|
||||
|
||||
export const base64 = (data: string, mediaType: string, options?: AssetOptions) =>
|
||||
from({ type: "base64", data, mediaType }, options)
|
||||
|
||||
export const url = (
|
||||
value: string,
|
||||
options?: AssetOptions & Omit<Extract<Source, { readonly type: "url" }>, "type" | "url">,
|
||||
) => {
|
||||
const { mediaType, expiresAt, ...rest } = options ?? {}
|
||||
return from({ type: "url", url: value, mediaType, expiresAt }, rest)
|
||||
}
|
||||
|
||||
export const ref = (provider: string | ProviderID, id: string, mediaType?: string, options?: AssetOptions) =>
|
||||
from({ type: "ref", provider: ProviderID.make(provider), id, mediaType }, options)
|
||||
|
||||
const DATA_URL = /^data:([^;,]+)(?:;[^,]*)*;base64,(.*)$/s
|
||||
|
||||
/** Parse a `data:<mime>;base64,<data>` URL, or `undefined` when the value is not a base64 data URL. */
|
||||
export const parseDataUrl = (value: string, options?: AssetOptions) => {
|
||||
const match = DATA_URL.exec(value)
|
||||
return match === null ? undefined : base64(match[2], match[1], options)
|
||||
}
|
||||
|
||||
/** Parse a `data:<mime>;base64,<data>` URL. Malformed input throws a typed `AIError` because constructors are sync. */
|
||||
export const fromDataUrl = (dataUrl: string, options?: AssetOptions) => {
|
||||
const asset = parseDataUrl(dataUrl, options)
|
||||
if (asset === undefined) throw invalid("Media data URLs must contain a MIME type and base64 data")
|
||||
return asset
|
||||
}
|
||||
|
||||
/** Read a file through `FileSystem` and sniff its media type from magic bytes, then the extension. */
|
||||
export const file = (path: string, options?: AssetOptions): Effect.Effect<Asset, AIError, FileSystem.FileSystem> =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const data = yield* fs
|
||||
.readFile(path)
|
||||
.pipe(Effect.mapError((cause) => invalid(`Failed to read media file ${path}`, cause)))
|
||||
return bytes(data, fileMediaType(data, path), options)
|
||||
})
|
||||
|
||||
/** Materialize an asset and write its bytes through `FileSystem`. */
|
||||
export const write = (asset: Asset, path: string): Effect.Effect<void, AIError, FileSystem.FileSystem | Service> =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const data = yield* asset.bytes()
|
||||
yield* fs
|
||||
.writeFile(path, data)
|
||||
.pipe(Effect.mapError((cause) => invalid(`Failed to write media file ${path}`, cause)))
|
||||
})
|
||||
@@ -1,214 +0,0 @@
|
||||
import { Effect, Layer, ManagedRuntime, Stream } from "effect"
|
||||
import { AIClient } from "./ai-client.js"
|
||||
import type { AwaitOptions, Event, Generation, Snapshot } from "./generation.js"
|
||||
import { Image, type ImageModel, type ImageRequest, type ImageRequestInput } from "./image.js"
|
||||
import { LLM } from "./index.js"
|
||||
import { Media } from "./media.js"
|
||||
import { tryRequest } from "./media-model.js"
|
||||
import { RequestExecutor } from "./route/executor.js"
|
||||
import { AIError, InvalidRequestError, LanguageModel, LLMRequest } from "./schema/index.js"
|
||||
import type { RequestInput } from "./llm.js"
|
||||
import { Speech, type SpeechModel, type SpeechRequest, type SpeechRequestInput } from "./speech.js"
|
||||
import {
|
||||
Transcription,
|
||||
type TranscriptionModel,
|
||||
type TranscriptionRequest,
|
||||
type TranscriptionRequestInput,
|
||||
} from "./transcription.js"
|
||||
import { fileMediaType } from "./utils/media-type.js"
|
||||
import { Video, type VideoModel, type VideoRequest, type VideoRequestInput } from "./video.js"
|
||||
|
||||
/**
|
||||
* Promise-first entrypoint for scripts and non-Effect callers. One `ManagedRuntime` hosts the LLM, image, video, speech,
|
||||
* and transcription clients over a request executor; every method runs the corresponding Effect API and rethrows
|
||||
* `AIError` unchanged. `file` and `write` load `node:fs/promises` on first use, so importing this module does not.
|
||||
*/
|
||||
export interface Options {
|
||||
/** Executor layer; defaults to `RequestExecutor.fetchLayer`. Inject a recorder or `RequestExecutor.middleware(fn)` here. */
|
||||
readonly layer?: Layer.Layer<RequestExecutor.Service>
|
||||
}
|
||||
|
||||
export interface RunOptions {
|
||||
readonly signal?: AbortSignal
|
||||
}
|
||||
|
||||
export type Services = AIClient.Services
|
||||
|
||||
/**
|
||||
* Promise view of a `Generation`. Its fields are a snapshot taken when the handle was created; `refresh()` resolves to a
|
||||
* new handle rather than updating this one.
|
||||
*/
|
||||
export type GenerationHandle<Response> = Snapshot & {
|
||||
/** Serializable JSON; pass it back to `resume` from another process. */
|
||||
readonly token: unknown
|
||||
readonly await: (options?: AwaitOptions & RunOptions) => Promise<Response>
|
||||
/** Status observations until the first terminal one, polling like `await`; abort throws `signal.reason`. */
|
||||
readonly events: (options?: AwaitOptions & RunOptions) => AsyncIterable<Event>
|
||||
/** The result without polling; fails when the generation has not completed. */
|
||||
readonly result: (options?: RunOptions) => Promise<Response>
|
||||
readonly refresh: (options?: RunOptions) => Promise<GenerationHandle<Response>>
|
||||
readonly cancel: (options?: RunOptions) => Promise<void>
|
||||
}
|
||||
|
||||
// Fails with `signal.reason` so aborted calls reject and aborted streams throw like `fetch`: an `AbortError` by default.
|
||||
const abortEffect = (signal: AbortSignal | undefined) =>
|
||||
signal === undefined
|
||||
? Effect.never
|
||||
: Effect.callback<never, unknown>((resume) => {
|
||||
if (signal.aborted) {
|
||||
resume(Effect.fail(signal.reason))
|
||||
return
|
||||
}
|
||||
const onAbort = () => resume(Effect.fail(signal.reason))
|
||||
signal.addEventListener("abort", onAbort, { once: true })
|
||||
return Effect.sync(() => signal.removeEventListener("abort", onAbort))
|
||||
})
|
||||
|
||||
export const make = (options: Options = {}) => {
|
||||
const runtime = ManagedRuntime.make(AIClient.layerWith(options.layer ?? RequestExecutor.fetchLayer))
|
||||
|
||||
/** Run any package Effect (for example `LLMClient.compact(...)`) inside this runtime. */
|
||||
const run = <A, E>(effect: Effect.Effect<A, E, Services>, options?: RunOptions) =>
|
||||
runtime.runPromise(Effect.raceFirst(effect, abortEffect(options?.signal)))
|
||||
|
||||
const iterate = <A, E>(stream: Stream.Stream<A, E, Services>, options?: RunOptions): AsyncIterable<A> =>
|
||||
Stream.toAsyncIterable(
|
||||
Stream.unwrap(
|
||||
runtime.contextEffect.pipe(
|
||||
Effect.map(
|
||||
(context): Stream.Stream<A, unknown> =>
|
||||
stream.pipe(Stream.interruptWhen(abortEffect(options?.signal)), Stream.provideContext(context)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const handle = <Response>(generation: Generation<Response>): GenerationHandle<Response> => ({
|
||||
...generation.snapshot,
|
||||
token: generation.token,
|
||||
await: (options) => run(generation.await({ poll: options?.poll }), options),
|
||||
events: (options) => iterate(generation.events({ poll: options?.poll }), options),
|
||||
result: (options) => run(generation.result(), options),
|
||||
refresh: (options) => run(generation.refresh(), options).then(handle),
|
||||
cancel: (options) => run(generation.cancel(), options),
|
||||
})
|
||||
|
||||
const llmRequest = (input: RequestInput | LLMRequest) =>
|
||||
input instanceof LLMRequest ? Effect.succeed(input) : tryRequest(() => LLM.request(input))
|
||||
|
||||
return {
|
||||
run,
|
||||
/** Decoded asset bytes, downloading `url` sources through the executor. */
|
||||
bytes: (asset: Media.Asset, options?: RunOptions) => run(asset.bytes(), options),
|
||||
base64: (asset: Media.Asset, options?: RunOptions) => run(asset.base64(), options),
|
||||
/** Pull a `url` asset into owned bytes before the provider URL expires. */
|
||||
materialize: (asset: Media.Asset, options?: RunOptions) => run(asset.materialize(), options),
|
||||
/** Read a file into an asset like `Media.file`: sniffed media type, then the extension's. */
|
||||
file: async (path: string, options?: Media.AssetOptions & RunOptions) => {
|
||||
const { readFile } = await import("node:fs/promises")
|
||||
return run(
|
||||
Effect.tryPromise({
|
||||
try: (signal) => readFile(path, { signal }),
|
||||
catch: (cause) => fileError(`Failed to read media file ${path}`, cause),
|
||||
}).pipe(
|
||||
Effect.map((buffer) => {
|
||||
const data = new Uint8Array(buffer)
|
||||
return Media.bytes(data, fileMediaType(data, path), options)
|
||||
}),
|
||||
),
|
||||
options,
|
||||
)
|
||||
},
|
||||
/** Write an asset's bytes like `Media.write`, downloading `url` sources through the executor. */
|
||||
write: async (asset: Media.Asset, path: string, options?: RunOptions) => {
|
||||
const { writeFile } = await import("node:fs/promises")
|
||||
return run(
|
||||
asset.bytes().pipe(
|
||||
Effect.flatMap((data) =>
|
||||
Effect.tryPromise({
|
||||
try: (signal) => writeFile(path, data, { signal }),
|
||||
catch: (cause) => fileError(`Failed to write media file ${path}`, cause),
|
||||
}),
|
||||
),
|
||||
),
|
||||
options,
|
||||
)
|
||||
},
|
||||
llm: {
|
||||
request: LLM.request,
|
||||
generate: <const Model extends LanguageModel>(input: RequestInput<Model> | LLMRequest, options?: RunOptions) =>
|
||||
run(Effect.flatMap(llmRequest(input), LLM.generate), options),
|
||||
stream: <const Model extends LanguageModel>(input: RequestInput<Model> | LLMRequest, options?: RunOptions) =>
|
||||
iterate(Stream.unwrap(Effect.map(llmRequest(input), LLM.stream)), options),
|
||||
},
|
||||
image: {
|
||||
request: Image.request,
|
||||
generate: <const Model extends ImageModel>(
|
||||
input: ImageRequestInput<Model> | ImageRequest,
|
||||
options?: AwaitOptions & RunOptions,
|
||||
) => run(Image.generate(input, { poll: options?.poll }), options),
|
||||
stream: <const Model extends ImageModel>(
|
||||
input: ImageRequestInput<Model> | ImageRequest,
|
||||
options?: AwaitOptions & RunOptions,
|
||||
) => iterate(Image.stream(input, { poll: options?.poll }), options),
|
||||
start: <const Model extends ImageModel>(input: ImageRequestInput<Model> | ImageRequest, options?: RunOptions) =>
|
||||
run(Image.start(input), options).then(handle),
|
||||
resume: (model: ImageModel, token: unknown, options?: RunOptions) =>
|
||||
run(Image.resume(model, token), options).then(handle),
|
||||
},
|
||||
video: {
|
||||
request: Video.request,
|
||||
start: <const Model extends VideoModel>(input: VideoRequestInput<Model> | VideoRequest, options?: RunOptions) =>
|
||||
run(Video.start(input), options).then(handle),
|
||||
generate: <const Model extends VideoModel>(
|
||||
input: VideoRequestInput<Model> | VideoRequest,
|
||||
options?: AwaitOptions & RunOptions,
|
||||
) => run(Video.generate(input, { poll: options?.poll }), options),
|
||||
resume: (model: VideoModel, token: unknown, options?: RunOptions) =>
|
||||
run(Video.resume(model, token), options).then(handle),
|
||||
stream: <const Model extends VideoModel>(
|
||||
input: VideoRequestInput<Model> | VideoRequest,
|
||||
options?: AwaitOptions & RunOptions,
|
||||
) => iterate(Video.stream(input, { poll: options?.poll }), options),
|
||||
},
|
||||
speech: {
|
||||
request: Speech.request,
|
||||
generate: <const Model extends SpeechModel>(
|
||||
input: SpeechRequestInput<Model> | SpeechRequest,
|
||||
options?: RunOptions,
|
||||
) => run(Speech.generate(input), options),
|
||||
stream: <const Model extends SpeechModel>(
|
||||
input: SpeechRequestInput<Model> | SpeechRequest,
|
||||
options?: RunOptions,
|
||||
) => iterate(Speech.stream(input), options),
|
||||
},
|
||||
transcription: {
|
||||
request: Transcription.request,
|
||||
generate: <const Model extends TranscriptionModel>(
|
||||
input: TranscriptionRequestInput<Model> | TranscriptionRequest,
|
||||
options?: AwaitOptions & RunOptions,
|
||||
) => run(Transcription.generate(input, { poll: options?.poll }), options),
|
||||
stream: <const Model extends TranscriptionModel>(
|
||||
input: TranscriptionRequestInput<Model> | TranscriptionRequest,
|
||||
options?: AwaitOptions & RunOptions,
|
||||
) => iterate(Transcription.stream(input, { poll: options?.poll }), options),
|
||||
start: <const Model extends TranscriptionModel>(
|
||||
input: TranscriptionRequestInput<Model> | TranscriptionRequest,
|
||||
options?: RunOptions,
|
||||
) => run(Transcription.start(input), options).then(handle),
|
||||
resume: (model: TranscriptionModel, token: unknown, options?: RunOptions) =>
|
||||
run(Transcription.resume(model, token), options).then(handle),
|
||||
},
|
||||
dispose: () => runtime.dispose(),
|
||||
}
|
||||
}
|
||||
|
||||
export type Client = ReturnType<typeof make>
|
||||
|
||||
const fileError = (message: string, cause: unknown) =>
|
||||
new AIError({ reason: new InvalidRequestError({ message, cause }) })
|
||||
|
||||
/** Default client over `RequestExecutor.fetchLayer` for scripts; the runtime builds its layer on first use. */
|
||||
export const ai = make()
|
||||
|
||||
export * as AI from "./promise.js"
|
||||
Loaded 100 of 5828 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user