diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml deleted file mode 100644 index e93d5fbdb2..0000000000 --- a/.github/workflows/beta.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: beta - -on: - workflow_dispatch: - schedule: - - cron: "0 * * * *" - -jobs: - sync: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Setup Git Committer - id: setup-git-committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Install OpenCode - run: bun i -g opencode-ai - - - name: Sync beta branch - env: - GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - run: bun script/beta.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0368c44ee2..6dd110d90a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: inputs: release_version: - description: "Release version, without leading v. Example: 1.18.18+opencodez.1" + description: "Release version, without leading v. Example: 1.18.29+opencodez.1" required: true type: string binary_version: @@ -54,15 +54,15 @@ jobs: semver='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' if [[ ! "$release_version" =~ $semver ]]; then - echo "release_version must be semver-like, for example 1.18.18+opencodez.1" >&2 + echo "release_version must be semver-like, for example 1.18.29+opencodez.1" >&2 exit 1 fi if [[ "$release_version" != *opencodez* ]]; then - echo "release_version must be OpenCodez-specific, for example 1.18.18+opencodez.1" >&2 + echo "release_version must be OpenCodez-specific, for example 1.18.29+opencodez.1" >&2 exit 1 fi if [[ ! "$binary_version" =~ $semver ]]; then - echo "binary_version must be semver-like, for example 1.18.18+opencodez.1" >&2 + echo "binary_version must be semver-like, for example 1.18.29+opencodez.1" >&2 exit 1 fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42fab8be66..323e8c3b26 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,15 +3,16 @@ name: test on: push: branches: + - main - dev pull_request: - branches: [dev] + branches: [main, dev] workflow_dispatch: concurrency: - # Keep every run on dev so cancelled checks do not pollute the default branch - # commit history. PRs and other branches still share a group and cancel stale runs. - 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)) }} + # Keep every run on protected branches so cancelled checks do not pollute + # commit history. PRs and other branches share a group and cancel stale runs. + group: ${{ case(github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }} cancel-in-progress: true permissions: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index fc9a52797c..52a60c6093 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -2,9 +2,9 @@ name: typecheck on: push: - branches: [dev] + branches: [main, dev] pull_request: - branches: [dev] + branches: [main, dev] workflow_dispatch: jobs: diff --git a/.github/workflows/unlock.yml b/.github/workflows/unlock.yml new file mode 100644 index 0000000000..8df1af0e36 --- /dev/null +++ b/.github/workflows/unlock.yml @@ -0,0 +1,52 @@ +name: unlock + +on: + workflow_dispatch: + inputs: + stage: + description: SST stage to unlock + required: true + type: choice + options: + - dev + - production + +concurrency: deploy-${{ inputs.stage }} + +permissions: + contents: read + id-token: write + +jobs: + unlock: + runs-on: ubuntu-latest + environment: ${{ inputs.stage }} + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + - uses: ./.github/actions/setup-bun + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "24" + + - uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 + with: + role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }} + role-session-name: opencode-${{ github.run_id }} + aws-region: us-east-1 + + - run: bun sst unlock --stage=${{ inputs.stage }} + 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 }} + STRIPE_SECRET_KEY: ${{ inputs.stage == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }} + HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ vars.SENTRY_ORG }} + SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }} + SENTRY_RELEASE: unlock@${{ github.sha }} + VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }} + VITE_SENTRY_RELEASE: unlock@${{ github.sha }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d7ac31717..c99a25459f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,11 @@ - Made default-disabled FFF a true zero-index mode: OpenCodez no longer starts the upstream `rg --files` fallback or retains a background path index when `OPENCODE_DISABLE_FFF=1`. Directory browsing and explicit agent file tools remain available. - Made the v2 Web composer controls shrink and truncate inside narrow/mobile layouts, with icon-only Agent, System, and Variant controls and readable Model priority, so every control remains contained without changing desktop spacing. -- Updated the fork base from upstream OpenCode `1.18.13` to `1.18.18` while keeping the OpenCodez prompt, project, updater, embedded Web UI, Responses wire, and remote compaction layers isolated. +- Updated the fork base from upstream OpenCode `1.18.18` to `1.18.29` while keeping the OpenCodez prompt, project, updater, embedded Web UI, Responses wire, and remote compaction layers isolated. +- Added GPT-6 Astra model defaults and an adapted bundled System prompt from stable Codex `rust-v0.153.4`; Luna, Terra, and Sol now share the current Codex prompt while their existing selector names remain compatible. +- Added `opencodez.responses.context_window` for supported ChatGPT OAuth models. Astra keeps the 272k Codex working default and accepts an explicit value up to its advertised 872k client ceiling, with the remote-compaction trigger derived from the effective window. +- Aligned Responses Lite with the current Codex request contract by grouping ordinary tools under the `functions` namespace, assigning stable thread-scoped prefix IDs, sending model/tier routing hints, and falling back immediately to HTTP after WebSocket close code 1009. +- Bundled prompts now stay embedded in the binary instead of becoming stale copy-once user files. Exact unchanged legacy copies are removed on upgrade, while edited files remain user overrides. - Adopted upstream fixes for MCP SSE reconnect loops, configurable interleaved reasoning fields, stale prompt controls and session tabs, narrow file-tree tabs, directory selection, external desktop links, right-to-left layouts, localized desktop surfaces, and responsive worker-based Markdown rendering. - Adopted upstream chronological message boundaries across prompt execution, history revert and fork, App/TUI rendering, and shared-session views; OpenCodez remote-compaction boundaries now use the same persisted order instead of comparing raw IDs. - Adopted upstream's current project-scoped MCP state loading. @@ -19,6 +23,8 @@ - Closed the remaining remote-compaction recovery edges: newly completed tool output participates in the preflight limit, rejected follow-up requests retain the original active-turn boundary, pending steering stays out of the mandatory continuation, pre-turn replay preserves media, active request controls remain frozen through compaction, Stop cancels the HTTP operation through response-body processing, unsupported compact output artifacts are discarded, and opaque state is checked against its base API model and backend snapshot. Manual `/compact` now honors the selected model, and the UI reports completion only after success. - Persisted encrypted compaction now follows Codex across ChatGPT login changes: the new account receives one full request with the durable session state, while account-scoped response, reasoning, WebSocket, catalog, and retry state is never reused. - Made remote autocompaction media-aware and aligned its effective window with Codex's current 272k ChatGPT Responses context: inline images use model-visible token estimates instead of base64 text size, oversized compact requests bound older tool outputs across the request while preserving image media from the complete active parallel-tool batch, and failed compact attempts no longer render a successful completion divider. +- Fixed ChatGPT OAuth tool-result preflight to recognize persisted OpenCode image `FilePart` attachments before Responses lowering, preventing large native-vision data URLs from being counted as text and triggering false mid-turn compaction. +- Fixed Web clipboard, drag-and-drop, and file-picker attachments on ordinary LAN HTTP origins by using a random browser-draft blob ID when WebCrypto hashing is unavailable; asynchronous paste failures now use the existing error toast instead of failing silently. - Completed the Codex turn-routing contract for ChatGPT OAuth: same-turn sampling and remote compact retain sticky state from stream metadata, HTTP response headers, and WebSocket upgrade headers when the runtime exposes them across synthetic markers; a missing previous response performs one invisible full-request resync before model output; account affinity falls back safely to the OAuth subject when an account id is unavailable; and compact payload bounding preserves images from the complete active parallel-tool batch while rejecting requests that still cannot fit before network I/O. - Kept the TUI System indicator secondary and compact: it is hidden on narrow terminals, capped at 24 columns, and shows only System state. - ChatGPT OAuth now uses the Codex-compatible stateful Responses WebSocket wire by default. Set `opencodez.responses.wire` to `legacy` to restore the previous full-request flow. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ab14a7b62..a9c545efb8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -157,7 +157,7 @@ Caveats: - If `spawn` does not work for you, you can debug the server separately: - Debug server: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts serve --port 4096`, then attach TUI with `opencode attach http://localhost:4096` - - Debug TUI: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode --conditions=browser ./src/index.ts` + - Debug TUI: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts` Other tips and tricks: diff --git a/README.md b/README.md index d0dfe27893..006bd12479 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ OpenCodez is for people who want OpenCode to stay OpenCode, but with flexible pr | -------------- | ------------------------------------------------------------------------------------------------------------- | | Prompt control | TUI command and web composer control for the active System prompt. | | Prompt library | Upstream built-ins, bundled Codex presets, and user prompt files in one shared selector. | -| Model defaults | Configurable System defaults, with Codex-style defaults for OpenAI Responses GPT models out of the box. | +| Model defaults | Configurable System and context-window defaults for supported OpenAI Responses GPT models. | | Session state | A manual System choice stays with the session and does not reset on `/model`. | | Responses wire | ChatGPT OAuth can send incremental Codex-style WebSocket requests instead of resending the full conversation. | | Updates | `opencodez update` uses GitHub Releases. | @@ -48,10 +48,14 @@ OpenCodez keeps the normal OpenCode shape, but adds a few practical controls: - The TUI shows the concrete active System prompt id while you work. - ChatGPT OAuth can use Codex-compatible stateful Responses WebSocket requests with safe full-request fallback. - ChatGPT OAuth uses server-side Responses compaction for long sessions and persists the opaque compacted context across restart and reconnect. +- GPT-6 Astra uses a 272k working window by default and supports an explicit ChatGPT context-window override up to its advertised 872k client ceiling. - Non-git projects stay scoped to the selected directory, explicit filesystem roots clamp to `$HOME`, and background file indexing is disabled by default. - `opencodez update` prints GitHub release, download progress, and install stages instead of staying silent during large asset downloads. +- Web image and file attachments work on ordinary LAN HTTP origins as well as + secure origins; clipboard, drag-and-drop, and file-picker input share the same + persisted browser draft store. OpenCodez is meant to be a small fork, not a full rebrand. Upstream internals, docs, workflows, integrations, and package surfaces should stay as close to OpenCode as practical unless a fork-specific change is genuinely needed. @@ -70,7 +74,7 @@ OpenCodez installs from GitHub Releases. It does not publish to npm and does not Linux and macOS install: ```bash -curl -fsSL https://raw.githubusercontent.com/Krablante/opencodez/main/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/Krablante/opencodez/main/install | bash ``` Windows PowerShell install: @@ -91,7 +95,19 @@ Check for updates without installing: opencodez update --check ``` -The update path is intentionally simple: GitHub Releases are the source of truth, the installer downloads the right release artifact for the current OS and architecture, and `opencodez update` uses the same release channel from inside the app. +The update path is intentionally simple: GitHub Releases are the source of truth, +the installer chooses the compatible OS, architecture, libc, and x64 CPU +baseline artifact, and `opencodez update` uses the same release channel from +inside the app. A production binary remembers its exact build target, so +self-update preserves `baseline` and `musl` instead of silently switching binary +flavor. On an older binary without that marker, update detects the host +conservatively. + +Both installers and self-update run the downloaded binary before replacement. +The reported version must match the release exactly and must have the production +form `X.Y.Z+opencodez.N`; `dev`, preview, beta, branch, and plain upstream +versions are rejected. `install.sh` remains a small compatibility redirect to +the canonical `install` script and contains no separate platform logic. During the download, `opencodez update` prints progress to stderr. When GitHub provides `Content-Length`, progress includes total MB and percent; otherwise it prints downloaded MB only. @@ -99,10 +115,6 @@ On Unix, OpenCodez asks for `sudo` only when the installed binary is in a protected system path such as `/usr/local/bin`. Installations older than `1.17.20+opencodez.2` need one bootstrap update with `sudo opencodez update`; later releases handle the protected target automatically. -Managed deployments may install the narrow `/usr/local/sbin/opencodez-install` -helper. When present, `opencodez update` uses it non-interactively to replace -only `/usr/local/bin/opencodez`; ordinary public installs keep the interactive -sudo fallback. If the installed binary is newer than the latest published release, `opencodez update` treats it as current instead of downgrading it. ## Run From Source @@ -116,11 +128,14 @@ For local development, run the source-checkout launcher directly: Production OpenCodez builds must explicitly set `OPENCODEZ_BUILD=1`, `OPENCODE_CHANNEL=latest`, and an `OPENCODE_VERSION` such as -`1.18.18+opencodez.1`. The build rejects missing, preview-channel, plain +`1.18.29+opencodez.1`. The build rejects missing, preview-channel, plain upstream-version, and other non-production OpenCodez metadata before generating an artifact. A valid build emits `opencodez-*` artifacts with an `opencodez` binary inside. -Normal public releases should use the `publish` GitHub Actions workflow. Give it an OpenCodez release version such as `1.18.18+opencodez.1`; the release version must include `opencodez` so accidental upstream-looking tags are rejected. The workflow embeds that complete version by default, typechecks the fork boundary, verifies generated-client drift, builds the `opencodez-*` assets, verifies their names and archive contents, uploads them to GitHub Releases, and publishes the release unless `draft` is enabled. +Development/source binaries are never valid install or deployment artifacts. +Do not copy them into an executable path or use them for a canary; build an +explicit production artifact first. +Normal public releases should use the `publish` GitHub Actions workflow. Give it an OpenCodez release version such as `1.18.29+opencodez.1`; the release version must include `opencodez` so accidental upstream-looking tags are rejected. The workflow embeds that complete version by default, typechecks the fork boundary, verifies generated-client drift, builds the `opencodez-*` assets, verifies their names and archive contents, uploads them to GitHub Releases, and publishes the release unless `draft` is enabled. ## Side-by-Side With OpenCode @@ -154,10 +169,14 @@ It covers System prompt defaults, Responses wire modes, config roots, session be Prompt library paths: ```text -~/.config/opencodez/prompts/core/.md +~/.config/opencodez/prompts/core/.md # user overrides ``` -Bundled Codex-derived prompt files use the `codex_` prefix. User-created prompt files do not need that prefix. +Bundled Codex-derived prompts are embedded in the binary and use the `codex_` +prefix. A user file with the same name overrides its bundled prompt; user-created +files do not need that prefix. On upgrade, OpenCodez removes only unchanged files +created by the retired copy-once delivery mechanism, identified by their exact +content hash. Edited files remain user overrides. Bundled Core/System prompts: @@ -170,6 +189,7 @@ codex_gpt_5_4_mini codex_gpt_5_5 codex_gpt_5_6_luna_terra codex_gpt_5_6_sol +codex_gpt_6_astra ``` Out-of-the-box OpenAI Responses GPT System defaults: @@ -185,6 +205,7 @@ gpt-5.5 -> codex_gpt_5_5 gpt-5.6-luna -> codex_gpt_5_6_luna_terra gpt-5.6-terra -> codex_gpt_5_6_luna_terra gpt-5.6-sol -> codex_gpt_5_6_sol +gpt-6-astra -> codex_gpt_6_astra ``` Model defaults live in `~/.config/opencodez/opencode.jsonc`. Values can be one prompt name for all models, or a mapping keyed by model id, family, `provider/model`, or `default`: @@ -194,6 +215,7 @@ Model defaults live in `~/.config/opencodez/opencode.jsonc`. Values can be one p "opencodez": { "responses": { "wire": "codex", + "context_window": 872000, "compaction": { "threshold": 0.9, "token_limit": 300000, @@ -233,11 +255,12 @@ adapters, and other providers bypass Codex request lowering and keep their existing behavior. OpenCodez reads ChatGPT model capabilities from the authenticated Codex model -catalog and refreshes them on the catalog ETag. GPT-5.6 models that advertise -Responses Lite receive the Codex Lite request shape: tools and base instructions -move into developer input items, image `detail` hints are removed, reasoning -context covers all turns, and both HTTP and WebSocket requests carry the Lite -marker. A small built-in profile set keeps known models usable while the catalog +catalog and refreshes them on the catalog ETag. GPT-5.6 and GPT-6 models that +advertise Responses Lite receive the Codex Lite request shape: tools and base +instructions move into deterministic developer input items, ordinary tools are +grouped under the `functions` namespace, image `detail` hints are removed, +reasoning context covers all turns, and HTTP and WebSocket requests carry the +Lite marker and model/tier routing hint. A small built-in profile set keeps known models usable while the catalog is temporarily unavailable; it is a fallback, not the primary source of model context, automatic-compaction limits, `comp_hash`, or Lite support. @@ -262,7 +285,7 @@ safely under the new account. An expired OAuth token gets one response-driven refresh and safe retry before any model output, provided the refreshed account identity still matches the request; an identity change fails the attempt so the next request starts from canonical -session state. WebSocket upgrade status 426 switches that session to HTTP +session state. WebSocket upgrade status 426 or close code 1009 switches that session to HTTP immediately. A runtime that cannot expose the rejected upgrade status uses HTTP for the current request and waits one minute before probing WebSocket again. If account identity cannot be verified, OpenCodez uses uncached HTTP and the current @@ -295,9 +318,11 @@ crossing the threshold. Compact requests use the same effective System and tool schemas as sampling; steering input waits until the mandatory post-compact continuation even when provider-side overflow recovery follows a rejected request. Inline images use model-visible token estimates rather than their -base64 text size. If the complete compact payload is still too large, older tool -outputs are bounded across the request while images from the complete active -parallel-tool batch are preserved. +base64 text size. The same estimate applies to newly completed tool attachments +before provider lowering, so a native image result cannot trigger mid-turn +compaction merely because its data URL is large. If the complete compact payload +is still too large, older tool outputs are bounded across the request while +images from the complete active parallel-tool batch are preserved. Only verbatim tool output receives an additional estimation margin; ordinary text is not globally double-counted, and `detail: "original"` images use a safe 10,000-token maximum. When the backend does not include retained encrypted @@ -314,15 +339,24 @@ two retries on each transport, for at most six requests. Both honor a server `Retry-After` delay, remain cancellable with the session, and have no hidden retry multiplication; compaction still has no local-summary fallback. +`opencodez.responses.context_window` is an optional positive integer requested +for ChatGPT OAuth models. A model accepts the value only within its authenticated +catalog profile and clamps it to `max_context_window`; models without a larger +advertised ceiling cannot be enlarged. GPT-6 Astra has a 1,050,000-token API +context, defaults to the Codex 272,000-token working window, and currently +accepts an explicit override up to the Codex client ceiling of 872,000. Values +above 272,000 may use OpenAI long-context pricing. + `opencodez.responses.compaction.threshold` is a fraction of the model's input window and defaults to the Codex policy of `0.9`. It accepts values greater than `0` and no greater than `0.9`, so configuration can compact earlier but never later than the safe default. Optional `token_limit` adds an absolute positive token cap. The effective trigger is the minimum of the percentage limit, the absolute cap, OpenCode's usable-input limit, and the authenticated Codex model -catalog. The built-in fallback profiles use the `272000` context advertised by -Codex `rust-v0.146.0` for current Luna, Terra, and Sol models, producing a -`244800` default trigger when the catalog is temporarily unavailable. +catalog. The built-in fallback profiles mirror Codex `rust-v0.153.4`; Luna, +Terra, Sol, and Astra default to a `272000` working context and a `244800` +trigger. An Astra override of `872000` produces a `784800` trigger before any +lower threshold or absolute cap is applied. ## Commands diff --git a/bun.lock b/bun.lock index 04b5bcf35b..12e2a52f1b 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -78,7 +78,7 @@ "tailwindcss": "catalog:", }, "devDependencies": { - "@happy-dom/global-registrator": "20.0.11", + "@happy-dom/global-registrator": "20.12.0", "@playwright/test": "catalog:", "@sentry/vite-plugin": "catalog:", "@tailwindcss/vite": "catalog:", @@ -96,7 +96,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.18", + "version": "1.18.29", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -144,7 +144,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +158,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -178,6 +178,7 @@ "@upstash/redis": "1.38.0", "chart.js": "4.5.1", "nitro": "3.0.1-alpha.1", + "number-flow": "0.6.2", "solid-js": "catalog:", "solid-list": "0.3.0", "solid-stripe": "0.8.1", @@ -194,7 +195,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -221,7 +222,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -235,6 +236,7 @@ "devDependencies": { "@cloudflare/workers-types": "catalog:", "@tsconfig/node22": "22.0.2", + "@types/bun": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "openai": "5.11.0", @@ -243,7 +245,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -267,7 +269,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -287,21 +289,21 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.18", + "version": "1.18.29", "bin": { "opencode": "./bin/opencode", }, "dependencies": { "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.112", - "@ai-sdk/anthropic": "3.0.82", + "@ai-sdk/amazon-bedrock": "4.0.166", + "@ai-sdk/anthropic": "3.0.111", "@ai-sdk/azure": "3.0.88", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", "@ai-sdk/google": "3.0.73", - "@ai-sdk/google-vertex": "4.0.128", + "@ai-sdk/google-vertex": "4.0.181", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.51", "@ai-sdk/openai": "3.0.84", @@ -332,14 +334,14 @@ "@opentelemetry/sdk-trace-base": "2.6.1", "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", - "ai-gateway-provider": "3.1.2", + "ai-gateway-provider": "3.2.0", "bun-pty": "0.4.8", "cross-spawn": "catalog:", "diff": "catalog:", "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.1", + "gitlab-ai-provider": "6.13.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -381,7 +383,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@zip.js/zip.js": "2.7.62", "drizzle-orm": "catalog:", @@ -435,7 +437,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -449,7 +451,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "effect": "catalog:", }, @@ -461,7 +463,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -493,7 +495,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -509,7 +511,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -540,7 +542,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -559,7 +561,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.18", + "version": "1.18.29", "bin": { "opencode": "./bin/opencode", }, @@ -568,15 +570,15 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.21.0", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.112", - "@ai-sdk/anthropic": "3.0.82", + "@ai-sdk/amazon-bedrock": "4.0.166", + "@ai-sdk/anthropic": "3.0.111", "@ai-sdk/azure": "3.0.88", "@ai-sdk/cerebras": "2.0.60", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", "@ai-sdk/google": "3.0.73", - "@ai-sdk/google-vertex": "4.0.128", + "@ai-sdk/google-vertex": "4.0.181", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.51", "@ai-sdk/openai": "3.0.84", @@ -623,7 +625,7 @@ "@types/ws": "8.18.1", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", - "ai-gateway-provider": "3.1.2", + "ai-gateway-provider": "3.2.0", "bonjour-service": "1.3.0", "chokidar": "4.0.3", "cross-spawn": "catalog:", @@ -632,7 +634,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.1", + "gitlab-ai-provider": "6.13.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -690,7 +692,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -766,7 +768,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "cross-spawn": "catalog:", }, @@ -781,7 +783,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -796,7 +798,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", @@ -836,7 +838,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -849,7 +851,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -858,32 +860,25 @@ "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", "@solidjs/start": "catalog:", - "d3-geo": "3.1.1", "d3-scale": "4.0.2", "effect": "catalog:", "i18n-iso-countries": "7.14.0", "nitro": "3.0.1-alpha.1", "solid-js": "catalog:", "sst": "catalog:", - "topojson-client": "3.1.0", "vite": "catalog:", - "world-atlas": "2.0.2", }, "devDependencies": { "@cloudflare/workers-types": "catalog:", "@types/bun": "catalog:", - "@types/d3-geo": "3.1.0", "@types/d3-scale": "4.0.9", - "@types/geojson": "7946.0.16", - "@types/topojson-client": "3.1.5", - "@types/topojson-specification": "1.0.5", "@typescript/native-preview": "catalog:", "typescript": "catalog:", }, }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -902,7 +897,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -944,7 +939,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -971,7 +966,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1022,7 +1017,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -1071,10 +1066,12 @@ "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", + "@ai-sdk/anthropic@3.0.111": "patches/@ai-sdk%2Fanthropic@3.0.111.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", - "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", + "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", + "@ai-sdk/amazon-bedrock@4.0.166": "patches/@ai-sdk%2Famazon-bedrock@4.0.166.patch", "@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", @@ -1082,6 +1079,7 @@ "@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch", }, "overrides": { + "@ai-sdk/anthropic": "3.0.111", "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", @@ -1173,9 +1171,9 @@ "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.81", "@ai-sdk/openai": "3.0.67", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-PsSh7a6qW+3kQXPs1kD4wDwuZby0t1PIaB6j/1aMKmPFJ5LxcIcULLMF/bjITLt5o/8lc0t6TXIwG0zlhH7uZw=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.166", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.115", "@ai-sdk/openai": "3.0.105", "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.50", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HzzQb+ks+WmRcnnT9uBdJAxaqSXz3CWgmLSe9hePAkELPzTl/nAIJh2fb2+UsTD0n9OzZfBb25k68CIxraKkoA=="], - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.82", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WKKou2wbhGGYV8PSALAPyV2YY4nfCqCPkyBzYtJtDA9yCcIFwsbtkTNgg7bqtLCVzeEsY7wwxRoCWy+EMfrw/A=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.111", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-atgBW8jZPr/KuaKX5FvDIHuXBI8VCol6kVeoD4P0657+VXR73QsLogXQVN/Zt5FHtq9WzpdIZseCJiXPqkgwwA=="], "@ai-sdk/azure": ["@ai-sdk/azure@3.0.88", "", { "dependencies": { "@ai-sdk/deepseek": "2.0.47", "@ai-sdk/openai": "3.0.84", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RRjZkB1lYplh8dpBarnvkl1j7sYLHsyXua7erL3oNcMK7fHcl4bPO5C7iQhD1O/DqD/zCceDifnege1s+8yEvw=="], @@ -1183,21 +1181,21 @@ "@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqcCq2PiFY1dbK/0Ck45KuvE8jfdxRuuAE9Y5w46dAk6U+9vPOeg1CDcmR+ncqmrYrhRl3nmyDttyDahyjCzAw=="], - "@ai-sdk/deepgram": ["@ai-sdk/deepgram@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VscTV68g6sXRY4O1yl72/O8y6+tBDvSQax6bqX06hRKWBGxsJ8Jr3LZsNmZnK9Od5Icx565ijK0QgrlNaN4TdQ=="], + "@ai-sdk/deepgram": ["@ai-sdk/deepgram@2.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DPoKiCXDwzopJI/pHcZnXaycZ5qqCL4xCWcfsb1s8M8OUPchK2GUHcCXt6v056fAqtKWLF/hrW+RcHFFY0qyHQ=="], "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-y6RoOP7DGWmDSiSxrUSt5p18sbz+Ixe5lMVPmdE7x+Tr5rlrzvftyHhjWHfqlAtoYERZTGFbP6tPW1OfQcrb4A=="], "@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MzcQ321JO8OY+TVLFI81A7cIIuoeLLxrLCDD+8C1E3Ro6UFyfMtRXo9bw9OhTMRSDMo6hgSDOo4Fekz8aJtQYQ=="], - "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EtvsWfGrqx3OhzJdoi82qH+4yzEPPKZr2utyQ+w8cHKoFeg0+8Lou9Z3uixy73WEwz8Z1+AR8QT9fZ64AWGYPA=="], + "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XFONX6rAsu6d13cJVUfZfkq4a+qdThlxvoEfzYlSRa1AvALlzwX7Y6bunXxevuifT3n882+nDdWrdiYvFP0+Fw=="], - "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.53", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.48", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HjeiGsdxSzrCkOf2l2V+K+opzlqxBtduBq6BCiohAdgQk2KdZmI/67SMkBM6Kdze/BjUXiZlv0d7zNICPhxVDA=="], + "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.76", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.67", "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yg1ulgemh6BLMrs2vBNbtjV70NhenFI3Z7psB7s94FOEcGevvyV1qdFoqsgBZ7QyuUGwnC645c+eBFFtPAr5SQ=="], "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], "@ai-sdk/google": ["@ai-sdk/google@3.0.73", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o2MuIeyvZrFIeIbnbA8Thrr63irdyUBh0uWBZ2lY6yFeXuE/tcwyXF74bDKS4KvTu84uFpQfpbS/LXHGKKXz+g=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.128", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.77", "@ai-sdk/google": "3.0.73", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jK8fixb4km2yfgvb9DUFQRpV/jiDB0v9gyxHoHfPydaQvz+CpAz8DTt1quyaM+Wg9G2R8Zo68CYmHbIkUqW2AA=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.181", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.110", "@ai-sdk/google": "3.0.108", "@ai-sdk/openai-compatible": "2.0.67", "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-57b5Qor8V53vubkxCj09tbHWpzpCLUbzmll2FwShuLvyEAsCH6mh3sAowDhiwUWPXnLzU+rC3RVMKCPscqICcg=="], "@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="], @@ -1663,7 +1661,7 @@ "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], - "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.12.0", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.12.0" } }, "sha512-BUE55Rew3oMwBzwCwmUnV+Oxk51V3xolM39Ts6kGiBXNELjujiESwk9qc99JRr6cVrj3OTd9MJ5zQ2fpn+jy6g=="], "@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="], @@ -2843,8 +2841,6 @@ "@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="], - "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], - "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], @@ -2865,8 +2861,6 @@ "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], - "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], @@ -2945,10 +2939,6 @@ "@types/ssri": ["@types/ssri@7.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw=="], - "@types/topojson-client": ["@types/topojson-client@3.1.5", "", { "dependencies": { "@types/geojson": "*", "@types/topojson-specification": "*" } }, "sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw=="], - - "@types/topojson-specification": ["@types/topojson-specification@1.0.5", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ=="], - "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/tsscmp": ["@types/tsscmp@1.0.2", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="], @@ -3063,7 +3053,7 @@ "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], - "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], + "ai-gateway-provider": ["ai-gateway-provider@3.2.0", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.117", "@ai-sdk/anthropic": "^3.0.84", "@ai-sdk/azure": "^3.0.74", "@ai-sdk/cerebras": "^2.0.56", "@ai-sdk/cohere": "^3.0.38", "@ai-sdk/deepgram": "^2.0.35", "@ai-sdk/deepseek": "^2.0.38", "@ai-sdk/elevenlabs": "^2.0.35", "@ai-sdk/fireworks": "^2.0.56", "@ai-sdk/google": "^3.0.82", "@ai-sdk/google-vertex": "^4.0.145", "@ai-sdk/groq": "^3.0.41", "@ai-sdk/mistral": "^3.0.39", "@ai-sdk/openai": "^3.0.71", "@ai-sdk/perplexity": "^3.0.35", "@ai-sdk/xai": "^3.0.95", "@openrouter/ai-sdk-provider": "^2.10.0" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-IGSV96IqAfiZd20CWSMVQk5sVeLcJR2uQcoWLB8GdkxyvQrsU3x4U0o1Ok6bfVJug7SrlX6I8ibz9cHIkUyRtg=="], "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=="], @@ -3249,6 +3239,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], + "buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="], "builder-util": ["builder-util@26.15.0", "", { "dependencies": { "@types/debug": "^4.1.6", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-dUx+HxVbiNsNQ4mGe1PyoC/tBmsHwBNDLdBuqWCj+rhHFE9lHgrXiGYKAM1uNlznhAaUSyMlms84VeSSr3gOBA=="], @@ -3437,8 +3429,6 @@ "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], - "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], - "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], @@ -3667,6 +3657,8 @@ "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], @@ -3853,7 +3845,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.12.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Qn5iHqvjG8yktI5MWaUgdRR94l7O4WtYW0CAbhsCh1Tj0Fei/DeprOYPVyf4Nht1Ix6U2PXSYM32QOHI6Z2TDw=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.13.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-JDZhNjvoiB7xBfesNegXNDg6ItKf9M2l8/DifuIyuvYmGgaZnDDbl89QrOKSoIzAOpo/+3Om0qviBPm84nGEbg=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], @@ -3887,7 +3879,7 @@ "h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="], - "happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="], + "happy-dom": ["happy-dom@20.12.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-7uMYJu2SEwwL8vVcKp0C0lnt6d2LSGGe+T+oY79PiCJNNSgFpbxW8n5KuzpDQvrU4mt+fYiK1+Jy7Z2v39YR6g=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -4581,6 +4573,8 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "number-flow": ["number-flow@0.6.2", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-MCnImG4Q5vPwhSXnov56nOuyyKn6LC+Qd7II1UiKc+ACRtug5iAtn0+CwXNxM38AC5lSowEY+oYEtZX2qMnUyw=="], + "nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -5317,8 +5311,6 @@ "toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="], - "topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="], @@ -5561,8 +5553,6 @@ "workerd": ["workerd@1.20251118.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251118.0", "@cloudflare/workerd-darwin-arm64": "1.20251118.0", "@cloudflare/workerd-linux-64": "1.20251118.0", "@cloudflare/workerd-linux-arm64": "1.20251118.0", "@cloudflare/workerd-windows-64": "1.20251118.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Om5ns0Lyx/LKtYI04IV0bjIrkBgoFNg0p6urzr2asekJlfP18RqFzyqMFZKf0i9Gnjtz/JfAS/Ol6tjCe5JJsQ=="], - "world-atlas": ["world-atlas@2.0.2", "", {}, "sha512-IXfV0qwlKXpckz1FhwXVwKRjiIhOnWttOskm5CtxMsjgE/MXAYRHWJqgXOpM8IkcPBoXnyTU5lFHcYa5ChG0LQ=="], - "wrangler": ["wrangler@4.50.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.11", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251118.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251118.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251118.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-+nuZuHZxDdKmAyXOSrHlciGshCoAPiy5dM+t6mEohWm7HpXvTHmWQGUf/na9jjWlWJHCJYOWzkA1P5HBJqrIEA=="], "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], @@ -5641,21 +5631,19 @@ "@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.81", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA=="], - - "@ai-sdk/amazon-bedrock/@ai-sdk/openai": ["@ai-sdk/openai@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oAiGC9eWG7IgtdsdS74bOCnAAHarAfTJhWN9x5INwnWPekL802AvF+0I5DvLzIF1MIRmNw4N8mPSL/GUVbX9Mw=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/openai": ["@ai-sdk/openai@3.0.105", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.50" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-umx95F8gqGuhPdoW+4ofq83fuSN6aMHwISxvY6dZU51Iix5ZKId0FNru+0FQeu+xfFUrVNmf3vYQNc2FLN85xA=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.50", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YAcB+7M1JhAYsHorTrWyldCyZihjCKr/QRXH2vFrara/+lwqNE7q5KzoucKLZ7ktFiUonhnhFhRoiymsq/2K2Q=="], "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], "@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], - "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.46", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tEtld97plCFiYevsJuOkGkeuhQndeMWFBVrJS4AjnbD5AqrNSXRCe0p+BZ3Cju/sxDeeZ9ym3q9YUV8fASA7aQ=="], "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], @@ -5667,9 +5655,9 @@ "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/deepgram/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/deepgram/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], - "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], @@ -5677,27 +5665,27 @@ "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], - "@ai-sdk/elevenlabs/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/elevenlabs/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], - "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], - "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z9MC6M4Oh/yUY/F/eszOtO8wc2nMz99XmZQKd2gWTtyIfe716xTfrKe3aYZKg20NZDtyjqPPKPSR+wqz7q1T7Q=="], + "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-glcEJC2mBXJKj7joFI0fRhcbdDYKTBgXMPcT6Vcnlym67tTzuNG9pFx3zblxVv8TdOxhojJja5zGG19yeGJxuA=="], - "@ai-sdk/fireworks/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/fireworks/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], - "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], "@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], - "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.77", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ML8C2M1YvPA1ulEx4TiyF0k1xvC2ikEiPBIC1PPQ0a5xELUGrO2lAaEzsTEoJ+eCeDd8PSBuFJjs+r+9yIwQXA=="], + "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.108", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kwvYpRNghqt0VRKE7Hx1UWZQCUJJFqUITj24baxy+ApS0Hru0PkBJHD75a36Wc+e6e+wHcKR2MconTeJiBZigA=="], - "@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="], + "@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-glcEJC2mBXJKj7joFI0fRhcbdDYKTBgXMPcT6Vcnlym67tTzuNG9pFx3zblxVv8TdOxhojJja5zGG19yeGJxuA=="], - "@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], - "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], @@ -6171,21 +6159,21 @@ "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="], + "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.153", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.110", "@ai-sdk/openai": "3.0.96", "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iEXrLgWylCHJmznqlKLU3CqRh8UWibv+illrwmsk136FVBBvyXiGnpQrI1pGWCScVLQjBQSFQu7GJDkUEomf/A=="], - "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="], + "ai-gateway-provider/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="], - "ai-gateway-provider/@ai-sdk/azure": ["@ai-sdk/azure@3.0.49", "", { "dependencies": { "@ai-sdk/openai": "3.0.48", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wskgAL+OmrHG7by/iWIxEBQCEdc1mDudha/UZav46i0auzdFfsDB/k2rXZaC4/3nWSgMZkxr0W3ncyouEGX/eg=="], + "ai-gateway-provider/@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cXLjIsSzUriPHe704IH6d+ipJ/OvczTB700p9Zma7DPgQzvxG/diyr8q/2LEsbTRiTopiKhky8dn1PJNQcJToQ=="], - "ai-gateway-provider/@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9DhYurbAvcurOEGN6u2myYDybrrzGfcrkG8hwmFjwTrePW6KCMggm0YxP7e8RkLYcQKqCEMgFlyEB4BM6EmiKg=="], + "ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@3.0.108", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kwvYpRNghqt0VRKE7Hx1UWZQCUJJFqUITj24baxy+ApS0Hru0PkBJHD75a36Wc+e6e+wHcKR2MconTeJiBZigA=="], - "ai-gateway-provider/@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], + "ai-gateway-provider/@ai-sdk/groq": ["@ai-sdk/groq@3.0.59", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-X4h60TGq4pIOXPsthatUr+bfTaYCaKGX597hG9JgcueEl4+nboCdw99ixjFKGkvYlBJwLCCfI957EmGA2QlF0w=="], - "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], + "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], - "ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.82", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="], + "ai-gateway-provider/@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bL3SWrPltTuxVNg/bZ5APbJLVe0+BIU+BYAbT8Yo0eSAZ5eMTImN2murQHZa08Wi8lUMm8MNolaYBQkb0JDbFw=="], - "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="], + "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.10.0", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-FMsAEjLUt5pWuRE2LDC/LCvVrFjLlrEzUITH5+5SZtfq7KZ2wrOHjQVxzz92sju8S9ltpzW87CLW8/b0oBXVCw=="], "ajv-keywords/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], @@ -6511,8 +6499,6 @@ "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "tree-sitter-bash/node-addon-api": ["node-addon-api@8.8.0", "", {}, "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA=="], "tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -6585,8 +6571,12 @@ "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], + "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/anthropic/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], + "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -6595,16 +6585,24 @@ "@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/deepgram/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/elevenlabs/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/fireworks/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/google-vertex/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -6977,29 +6975,41 @@ "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/openai": ["@ai-sdk/openai@3.0.96", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Pex8vOj1y05j7jtBS39cJJRDjJbMIyCY9+01cSIp1hwEJTKImrFejMgsAazMWXSi/HU+B9ZE6ElftCOwvg4mmQ=="], - "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], + + "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], "ai-gateway-provider/@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], "ai-gateway-provider/@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "ai-gateway-provider/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="], + + "ai-gateway-provider/@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="], + + "ai-gateway-provider/@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="], + + "ai-gateway-provider/@ai-sdk/cohere/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], - "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "ai-gateway-provider/@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], - "ai-gateway-provider/@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="], + "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], - "ai-gateway-provider/@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], - "ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "ai-gateway-provider/@ai-sdk/groq/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], - "ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "ai-gateway-provider/@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], - "ai-gateway-provider/@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], - "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], + + "ai-gateway-provider/@ai-sdk/perplexity/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], + + "ai-gateway-provider/@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -7419,13 +7429,27 @@ "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "ai-gateway-provider/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "ai-gateway-provider/@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "ai-gateway-provider/@ai-sdk/cohere/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "ai-gateway-provider/@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "ai-gateway-provider/@ai-sdk/groq/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "ai-gateway-provider/@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "ai-gateway-provider/@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "ai-gateway-provider/@ai-sdk/perplexity/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/docs/opencodez.md b/docs/opencodez.md index 96579c4765..df726ec6f0 100644 --- a/docs/opencodez.md +++ b/docs/opencodez.md @@ -24,7 +24,7 @@ variables use the `OPENCODEZ_` prefix. Install from the public fork: ```bash -curl -fsSL https://raw.githubusercontent.com/Krablante/opencodez/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/Krablante/opencodez/main/install | bash ``` Update an installed binary from GitHub Releases: @@ -38,15 +38,23 @@ protected system path. Installations older than `1.17.20+opencodez.2` require one bootstrap run with `sudo opencodez update`; later releases elevate only the atomic replacement step automatically. -Managed hosts can install `/usr/local/sbin/opencodez-install` with a scoped -sudoers rule. The updater prefers that helper for a non-interactive atomic -replacement of `/usr/local/bin/opencodez`, then falls back to the normal -interactive sudo flow when the helper is absent. - Release versions use the upstream base plus OpenCodez build metadata, for -example `1.18.18+opencodez.1`. The release tag and embedded binary version must +example `1.18.29+opencodez.1`. The release tag and embedded binary version must match exactly. +The canonical Unix installer detects OS, architecture, musl versus glibc, and +whether an x64 CPU needs the baseline build. The PowerShell installer applies +the same conservative x64 baseline decision. Production binaries embed their +complete target name, so self-update preserves an installed `baseline` or +`musl` flavor exactly; binaries predating that marker fall back to host +detection. `install.sh` is only a compatibility redirect to the canonical +installer and carries no second copy of the selection logic. + +Installers and self-update execute the candidate before replacement and require +its version to match the release exactly in `X.Y.Z+opencodez.N` form. A `dev`, +local, beta, preview, branch, or plain upstream binary is never a valid +installed, deployed, canary, or operator artifact. + ## System Prompt Library OpenCodez has one managed prompt concept: System Prompt. User prompt files live @@ -56,9 +64,17 @@ in: ~/.config/opencodez/prompts/core/.md ``` -Bundled files are copied only when missing, so user edits are preserved. The -selector combines bundled Codex prompts, upstream OpenCode built-ins, and custom -files from this directory. +Bundled prompts are embedded in the binary. The selector combines them with +upstream OpenCode built-ins and custom files from this directory, using +user file > OpenCodez bundled > OpenCode builtin precedence. During migration +from the retired copy-once delivery mechanism, OpenCodez removes only files that +still match an old bundled content hash exactly. Edited files remain overrides. +That migration is deduplicated once per active config root. Model requests read +the selected file directly instead of rebuilding and sorting the complete +library. If a selected user file disappears or becomes unreadable, request +preparation fails visibly rather than silently substituting an upstream System +prompt; Web selection and refresh failures are also shown in the existing error +surface. The same System selector is available in both generations of the Web/Desktop prompt composer. New sessions carry the selected prompt in submission metadata; @@ -68,6 +84,10 @@ change. Session creation carries the same optional metadata through both the current Protocol API and the App compatibility adapter, so a selection made before the first message survives creation, reload, and later protocol upgrades. +Persisted session metadata is authoritative. The process keeps only the 256 +most recently used selections as a short-lived UI bridge, removes stale entries +when metadata no longer contains a selection, and clears an entry when its +session is deleted. The Web prompt control stays in the compact model-control row. In the TUI the same selection is secondary status: it is rendered as `S: `, omitted @@ -86,12 +106,15 @@ codex_gpt_5_4_mini codex_gpt_5_5 codex_gpt_5_6_luna_terra codex_gpt_5_6_sol +codex_gpt_6_astra ``` -The GPT 5.5, GPT 5.6 Luna/Terra, and GPT 5.6 Sol prompts come from the official -Codex `rust-v0.144.1` model catalog. Luna and Terra share one prompt. Sol has a -separate prompt. The official GPT 5.6 Personality sections remain embedded in -their System prompts. +The current Codex-derived prompts are pinned to stable Codex `rust-v0.153.4`. +Luna, Terra, and Sol now share the same upstream prompt while retaining both +legacy selector names. The Astra prompt follows the same catalog template with +only unavailable Codex-product contracts removed or renamed for OpenCodez tools. +`codex_gpt_5_3_codex` remains a legacy compatibility prompt because the current +Codex catalog no longer publishes that model asset. ## Model Defaults @@ -109,6 +132,7 @@ gpt-5.5 -> codex_gpt_5_5 gpt-5.6-luna -> codex_gpt_5_6_luna_terra gpt-5.6-terra -> codex_gpt_5_6_luna_terra gpt-5.6-sol -> codex_gpt_5_6_sol +gpt-6-astra -> codex_gpt_6_astra ``` Users can override defaults in `~/.config/opencodez/opencode.jsonc`: @@ -118,6 +142,7 @@ Users can override defaults in `~/.config/opencodez/opencode.jsonc`: "opencodez": { "responses": { "wire": "codex", + "context_window": 872000, "compaction": { "threshold": 0.9, "token_limit": 300000, @@ -135,8 +160,8 @@ Users can override defaults in `~/.config/opencodez/opencode.jsonc`: ## Responses Wire Mode -ChatGPT OAuth sessions can use the stateful Responses WebSocket protocol from -Codex `rust-v0.146.0`. The first request in a fresh transport chain sends the +ChatGPT OAuth sessions can use the stateful Responses WebSocket protocol aligned +with Codex `rust-v0.153.4`. The first request in a fresh transport chain sends the full conversation. Later compatible requests send only new input items together with `previous_response_id` when the conversation prefix and model settings still match. This continuation can cross a logical user-turn boundary, while @@ -166,12 +191,16 @@ uses known fallback profiles for one minute instead of blocking model access; an uncached failure applies that fallback only to its current request. When a model advertises Responses Lite, OpenCodez applies the Codex Lite shape at -the authenticated fetch boundary. Tools become an `additional_tools` developer +the authenticated fetch boundary. Ordinary functions and custom tools are +grouped into the `functions` namespace inside an `additional_tools` developer item, base instructions become a developer message, top-level instructions are empty, parallel tool calls are disabled, reasoning context is `all_turns`, and -image `detail` fields are removed. HTTP carries the Lite header and WebSocket -requests carry the matching client-metadata marker. Legacy wire mode bypasses -this transformation. +image `detail` fields are removed. The two synthetic prefix items use stable +thread-scoped UUIDv5 identifiers derived from their visible payload, preserving +identity across retries and resume without persistent state. HTTP carries the +Lite header and WebSocket requests carry the matching client-metadata marker. +Both transports receive `x-codex-routing-hint` with the model and optional +service tier. Legacy wire mode bypasses this transformation. Fast aliases supplied by the model catalog keep the base model id and use `service_tier: "priority"`. ChatGPT's Codex backend also requires the Codex @@ -200,7 +229,9 @@ and returns it in subsequent sampling and remote-compaction requests. Bun's client exposes neither rejected-upgrade status nor headers, so standalone binaries switch an opaque non-101 handshake directly to HTTP for that request and wait one minute before probing WebSocket again. Exact 426 responses and a -fully exhausted WebSocket retry cycle remain session-sticky HTTP fallbacks. This +WebSocket close code 1009 and a fully exhausted retry cycle remain session-sticky +HTTP fallbacks. A 1009 switches the current request immediately instead of +spending the remaining WebSocket retry budget. This keeps transient authentication and server failures recoverable without adding a second socket stack or repeatedly probing an unsupported endpoint. The logical turn ID survives synthetic compaction markers and is intentionally @@ -281,7 +312,7 @@ Automatic and manual compaction use Codex Remote Compaction V2 for ChatGPT OAuth. OpenCodez sends a normal streamed `/responses` request through the same transport as sampling and appends exactly one `compaction_trigger` input item. The server must complete the stream with exactly one opaque compaction item. -This is the default ChatGPT wire implementation in Codex `rust-v0.146.0`; the +This is the default ChatGPT wire implementation in Codex `rust-v0.153.4`; the public [OpenAI compaction guide](https://developers.openai.com/api/docs/guides/compaction) documents the related public API through `context_management` and standalone `/responses/compact`, not this Codex-specific trigger. OpenCodez does not ask the @@ -335,7 +366,11 @@ treated as mid-turn and is included rather than discarded. Before a follow-up request, OpenCodez also adds newly completed tool-output size to the provider's last usage. A large result can therefore start mid-turn compaction before the provider rejects the next request; if rejection still occurs, the durable -assistant/tool progress keeps recovery in the same mid-turn phase. +assistant/tool progress keeps recovery in the same mid-turn phase. This +preflight recognizes both persisted OpenCode `FilePart` image attachments and +their lowered Responses `input_image` form. Inline image data therefore uses the +same media estimate at both boundaries instead of being counted as base64 text +before provider lowering. The active runner freezes the prepared effective System, transformed options, and model-visible tool schemas used for sampling, then reuses that snapshot for @@ -356,8 +391,8 @@ conservative 10,000-patch maximum. Only when the payload would exceed the Codex-safe request window does OpenCodez replace older tool outputs across the request with a bounded marker. The authenticated Codex model catalog supplies the current context and automatic compaction limit. Its fallback profiles mirror -Codex `rust-v0.146.0` with a `272000` context and a 90% trigger, so Luna, Terra, -and Sol use `244800` while the remote catalog is temporarily unavailable. Only +Codex `rust-v0.153.4` with a `272000` context and a 90% trigger, so Luna, Terra, +Sol, and Astra use `244800` while the remote catalog is temporarily unavailable. Only verbatim tool outputs receive a second conservative estimate because Codex caps them on insertion while OpenCode preserves them durably. Ordinary text is not globally doubled and can use the full safe window. Every tool result in the @@ -415,6 +450,19 @@ a successful compaction. #### Compaction Policy +`opencodez.responses.context_window` optionally requests a positive ChatGPT +OAuth working window. OpenCodez applies it only through the authenticated model +profile and clamps it to that profile's `max_context_window`; a model with no +larger advertised ceiling cannot be enlarged. The effective value is frozen in +the existing turn journal, so config or catalog changes cannot alter a running +tool loop. + +GPT-6 Astra exposes a 1,050,000-token API context and 128,000-token maximum +output. Codex defaults its working context to `272000` and currently permits a +config override up to `872000`; OpenAI long-context pricing begins above 272,000 +input tokens. OpenCodez follows those defaults. At `872000`, the default 90% +automatic-compaction trigger becomes `784800`. + `opencodez.responses.compaction.threshold` controls the fraction of the Codex-safe ChatGPT Responses context at which compaction runs. That context is the smaller of the provider input limit and the authenticated model catalog's @@ -430,7 +478,7 @@ is: min(min(provider_input_window, catalog_context) * threshold, token_limit when set, usable_input_limit) ``` -For the fallback Luna, Terra, and Sol profiles, the default trigger is `244800` +For the fallback Luna, Terra, Sol, and Astra profiles, the default trigger is `244800` tokens. Setting `threshold` to `0.8` moves it to `217600`; setting `token_limit` to `200000` lowers it further to `200000`. @@ -444,9 +492,10 @@ approximation would make compaction timing unstable. The fork-specific implementation stays inside the existing OpenAI plugin and OpenCodez config boundary: -- `packages/core/src/v1/config/opencodez.ts` defines the public wire and - compaction policy, while `packages/core/src/opencodez/settings.ts` owns the - `codex` and 90% defaults without embedding live model metadata. +- `packages/core/src/v1/config/opencodez.ts` defines the public wire, + context-window request, and compaction policy, while + `packages/core/src/opencodez/settings.ts` owns the `codex` and 90% defaults + without embedding live model metadata. - `packages/opencode/src/plugin/openai/codex.ts` enables the mode only when a request-scoped capability marker confirms ChatGPT OAuth and the official OpenAI adapter. It consumes that marker, supplies the Codex product originator @@ -465,10 +514,12 @@ OpenCodez config boundary: - `catalog.ts` owns an eight-account least-recently-used catalog map. Each account has an independent five-minute cache, in-flight refresh, and short scheduling barrier for upgrade-header ETags; a missing verified identity uses - an uncached request and cannot create reusable state. + an uncached request and cannot create reusable state. It also owns the + per-profile `max_context_window` clamp and derives the 90% compaction limit + when a supported model accepts a configured working window. - `request.ts` is the single raw request boundary for metadata, Responses Lite - shaping, persisted-state injection, and removal of the internal continuation - marker. + shaping, deterministic Lite prefix identifiers, routing hints, + persisted-state injection, and removal of the internal continuation marker. - `continuation.ts` owns prefix matching, response item normalization, `previous_response_id`, and continuation invalidation. Continuation is reused across compatible requests and logical user turns; the sticky routing token @@ -485,6 +536,11 @@ OpenCodez config boundary: after one minute and the process retains at most 128 handoffs. Direct mid-turn continuation uses one internal non-network marker to satisfy the AI SDK's non-empty prompt validation; the request adapter removes it before network I/O. +- `session-compaction.ts` owns the complete fork-specific remote-compaction + lifecycle: turn-profile journaling, transition detection, overflow accounting, + remote request preparation and retry, opaque-state persistence, and pending + input recovery. The upstream `session/compaction.ts` keeps local summary and + common marker behavior and delegates the remote branch through this one seam. - `attempt.ts` owns request kinds, bounded retry policy, and the lightweight sampling-attempt journal. It records only part IDs, permits rollback before a tool-call side-effect barrier, resumes from durable tool results without @@ -506,10 +562,11 @@ OpenCodez config boundary: the prepared request controls through a pending mid-turn compact without a second storage layer. - `packages/opencode/src/session/prompt.ts` owns the explicit follow-up and - durable-output decision, while `packages/opencode/src/session/compaction.ts` - owns pre-turn replay, previous-model-first hash transitions with one - current-model fallback, complete mid-turn history, and the pending-input - boundary through the first post-compact continuation. + durable-output decision. Shared marker creation, local summarization, and + auto-continue stay in `packages/opencode/src/session/compaction.ts`; the + fork-owned remote module owns pre-turn replay, previous-model-first hash + transitions with one current-model fallback, complete mid-turn history, and + the pending-input boundary through the first post-compact continuation. - The transport pool retains at most 256 session entries; transport fallback remains sticky until explicit removal or bounded eviction. Continuation resets on reconnect, login change, abort, failure, or concurrent @@ -600,6 +657,15 @@ desktop spacing remains unchanged; Agent, System, and Variant become icon-only below the `sm` breakpoint so the Model stays readable and the Send action never leaves the composer. +Browser drafts persist attachments in IndexedDB. Secure origins use SHA-256 +content IDs, preserving byte-level duplicate detection. Browsers do not expose +`crypto.subtle` to ordinary LAN HTTP origins, so those origins use random +128-bit IDs from `crypto.getRandomValues` instead. This keeps clipboard paste, +drag-and-drop, and file-picker attachments operational without a hashing +dependency, an additional file scan, or an HTTPS deployment requirement. +Byte-level duplicate detection is unavailable on insecure origins; attachment +persistence and submission behavior are otherwise unchanged. + OpenCodez can embed the built web UI as one packed binary asset. Runtime delivery unpacks it in memory, applies SPA fallback, preserves MIME types, serves compressed variants, and emits `Vary: Accept-Encoding`. A versioned on-disk UI @@ -637,6 +703,7 @@ packages/opencode/src/opencodez/codex-responses/compaction.ts packages/opencode/src/opencodez/codex-responses/continuation.ts packages/opencode/src/opencodez/codex-responses/protocol.ts packages/opencode/src/opencodez/codex-responses/request.ts +packages/opencode/src/opencodez/codex-responses/session-compaction.ts packages/opencode/src/opencodez/codex-responses/transport.ts packages/opencode/src/plugin/openai/ws.ts packages/opencode/src/session/llm/request.ts @@ -644,6 +711,9 @@ packages/opencode/src/server/routes/instance/httpapi/groups/opencodez.ts packages/opencode/src/server/routes/instance/httpapi/handlers/opencodez.ts packages/tui/src/component/opencodez-dialogs.tsx packages/app/src/components/prompt-input.tsx +packages/app/src/utils/draft-store.ts +packages/session-ui/src/v2/components/prompt-input/attachments.ts +packages/session-ui/src/v2/components/prompt-input/interaction.ts packages/session-ui/src/v2/components/prompt-input/index.tsx ``` diff --git a/infra/console.ts b/infra/console.ts index 79556f5e0c..764807d978 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -221,6 +221,15 @@ 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!) }, }) +// Preview branches have independent databases; do not send their workspaces to shared dev. +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 STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", { properties: { value: stripeWebhook.secret }, }) @@ -255,6 +264,7 @@ new sst.cloudflare.x.SolidStart("Console", { SECRET.UpstashRedisRestUrl, SECRET.UpstashRedisRestToken, AUTH_API_URL, + consoleMigration, STRIPE_WEBHOOK_SECRET, SECRET.SupportApiKey, DISCORD_INCIDENT_WEBHOOK_URL, diff --git a/install b/install index 1f868dca0c..66d6d62e8b 100755 --- a/install +++ b/install @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -euo pipefail APP=opencodez +REPOSITORY="${OPENCODEZ_UPDATE_REPOSITORY:-Krablante/opencodez}" MUTED='\033[0;2m' RED='\033[0;31m' @@ -11,7 +12,7 @@ usage() { cat <&2 + exit 1 +fi + print_message() { local level=$1 local message=$2 @@ -220,8 +226,6 @@ print_message() { check_version() { if command -v opencodez >/dev/null 2>&1; then - opencode_path=$(which opencodez) - ## Check the installed version installed_version=$(opencodez --version 2>/dev/null || echo "") @@ -234,6 +238,22 @@ check_version() { fi } +verify_production_binary() { + local binary="$1" + local expected="${2:-}" + local actual + actual=$("$binary" --version 2>/dev/null || true) + actual="${actual#v}" + if ! [[ "$actual" =~ ^[0-9]+\.[0-9]+\.[0-9]+\+opencodez\.[0-9]+$ ]]; then + echo -e "${RED}Error: Refusing to install non-production OpenCodez binary (reported version: ${actual:-unknown})${NC}" >&2 + exit 1 + fi + if [ -n "$expected" ] && [ "$actual" != "${expected#v}" ]; then + echo -e "${RED}Error: Downloaded binary reports ${actual}, expected ${expected#v}${NC}" >&2 + exit 1 + fi +} + unbuffered_sed() { if echo | sed -u -e "" >/dev/null 2>&1; then sed -nu "$@" @@ -340,13 +360,15 @@ download_and_install() { unzip -q "$tmp_dir/$filename" -d "$tmp_dir" fi + chmod 755 "$tmp_dir/opencodez" + verify_production_binary "$tmp_dir/opencodez" "$specific_version" mv "$tmp_dir/opencodez" "$INSTALL_DIR" - chmod 755 "${INSTALL_DIR}/opencodez" rm -rf "$tmp_dir" } install_from_binary() { print_message info "\n${MUTED}Installing ${NC}opencodez ${MUTED}from: ${NC}$binary_path" + verify_production_binary "$binary_path" cp "$binary_path" "${INSTALL_DIR}/opencodez" chmod 755 "${INSTALL_DIR}/opencodez" } diff --git a/install.ps1 b/install.ps1 index 9af18bcbe5..f034af3ae6 100644 --- a/install.ps1 +++ b/install.ps1 @@ -31,7 +31,19 @@ $Arch = switch ($ProcessorArchitecture.ToLowerInvariant()) { } $InstallDir = [System.Environment]::ExpandEnvironmentVariables($InstallDir) -$Asset = "opencodez-windows-$Arch.zip" +$Baseline = "" +if ($Arch -eq "x64") { + try { + Add-Type -MemberDefinition '[DllImport("kernel32.dll")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);' -Name Kernel32 -Namespace OpenCodezInstaller + if (-not [OpenCodezInstaller.Kernel32]::IsProcessorFeaturePresent(40)) { + $Baseline = "-baseline" + } + } catch { + # Unknown CPU capability must choose the broadly compatible artifact. + $Baseline = "-baseline" + } +} +$Asset = "opencodez-windows-$Arch$Baseline.zip" $Url = "https://github.com/$Repository/releases/latest/download/$Asset" $Work = Join-Path ([System.IO.Path]::GetTempPath()) "opencodez-install-$([System.Guid]::NewGuid().ToString("N"))" $Archive = Join-Path $Work $Asset @@ -77,6 +89,15 @@ try { New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null New-Item -ItemType Directory -Force -Path $Work | Out-Null + $Release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repository/releases/latest" -Headers @{ + Accept = "application/vnd.github+json" + "User-Agent" = "opencodez-installer" + } + $ExpectedVersion = ([string]$Release.tag_name).TrimStart("v") + if ($ExpectedVersion -notmatch '^\d+\.\d+\.\d+\+opencodez\.\d+$') { + throw "Refusing non-production OpenCodez release version: $ExpectedVersion" + } + $ProgressPreference = "SilentlyContinue" Write-Host "Downloading $Url" Invoke-WebRequest -Uri $Url -OutFile $Archive -UseBasicParsing @@ -92,6 +113,11 @@ try { $Binary = $Match.FullName } + $ActualVersion = ((& $Binary --version 2>&1) | Out-String).Trim().TrimStart("v") + if ($LASTEXITCODE -ne 0 -or $ActualVersion -ne $ExpectedVersion) { + throw "Downloaded binary reports '$ActualVersion'; expected production version '$ExpectedVersion'" + } + Copy-Item -Path $Binary -Destination (Join-Path $InstallDir "opencodez.exe") -Force if ($NoPath) { diff --git a/install.sh b/install.sh index f2dd8214de..fa6592ac4d 100755 --- a/install.sh +++ b/install.sh @@ -1,61 +1,15 @@ #!/usr/bin/env sh set -eu +# Compatibility entrypoint. Keep all platform and binary validation in the +# canonical Bash installer so the two public URLs cannot drift. REPOSITORY="${OPENCODEZ_UPDATE_REPOSITORY:-Krablante/opencodez}" -INSTALL_DIR="${OPENCODEZ_INSTALL_DIR:-"$HOME/.local/bin"}" - -case "$(uname -s | tr '[:upper:]' '[:lower:]')" in - linux) OS="linux" ;; - darwin) OS="darwin" ;; - *) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;; -esac - -case "$(uname -m)" in - x86_64 | amd64) ARCH="x64" ;; - arm64 | aarch64) ARCH="arm64" ;; - *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; -esac - -ASSET="opencodez-${OS}-${ARCH}" -WORK="${TMPDIR:-/tmp}/opencodez-install.$$" -ARCHIVE="${WORK}/asset" -BIN="${WORK}/opencodez" +WORK="${TMPDIR:-/tmp}/opencodez-installer.$$" cleanup() { - rm -rf "$WORK" + rm -f "$WORK" } trap cleanup EXIT -mkdir -p "$INSTALL_DIR" -mkdir -p "$WORK" - -FOUND="" -for suffix in ".tar.gz" ".zip" ""; do - URL="https://github.com/${REPOSITORY}/releases/latest/download/${ASSET}${suffix}" - echo "Trying ${URL}" - if curl -fsSL "$URL" -o "$ARCHIVE"; then - FOUND="${ASSET}${suffix}" - break - fi -done - -if [ -z "$FOUND" ]; then - echo "No matching OpenCodez release asset found for ${OS}/${ARCH}" >&2 - exit 1 -fi - -case "$FOUND" in - *.tar.gz) - tar -xzf "$ARCHIVE" -C "$WORK" opencodez - ;; - *.zip) - unzip -p "$ARCHIVE" opencodez > "$BIN" - ;; - *) - mv "$ARCHIVE" "$BIN" - ;; -esac - -chmod +x "$BIN" -mv "$BIN" "${INSTALL_DIR}/opencodez" -echo "Installed ${INSTALL_DIR}/opencodez" +curl -fsSL "https://raw.githubusercontent.com/${REPOSITORY}/main/install" -o "$WORK" +bash "$WORK" "$@" diff --git a/nix/hashes.json b/nix/hashes.json index 0864cb930d..b36141f400 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-TNwKfqxD83UpZuCKN8FdEWN+CcQUP9CkCQSLGNqR/sA=", - "aarch64-linux": "sha256-qzvOJZzmq2QhlauElw8GwgQnCPHdhexI52L0md5zrxQ=", - "aarch64-darwin": "sha256-ZzoyLayOFfcYUAg35ZbZ2WapxDdd9IUWqy2xkxZH4QM=", - "x86_64-darwin": "sha256-maP/qLeaC3q8VcmNIPyIKlnplxFXJ7ULho3v21/16Mw=" + "x86_64-linux": "sha256-2kzFIn42mD7ZDu/+6lWctqjZ/lIVZZfjZhmF/ymhF54=", + "aarch64-linux": "sha256-4HlReGD3gYfyyfnY/FQ46Ov+g3ZGV3sBYQ9p1bS9YAY=", + "aarch64-darwin": "sha256-dkfCoH/sW9XBPZ7XhnUoE54TY1lcfNvZ5wQQLr7gKiQ=", + "x86_64-darwin": "sha256-32t1JEcibZ4OrornIgfWOGQA87FOXjrxrxlDrflh7Ss=" } } diff --git a/package.json b/package.json index 0f11d0c396..8d58ace1b4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "module", "packageManager": "bun@1.3.14", "scripts": { - "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", + "dev": "bun run --cwd packages/opencode src/index.ts", "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", @@ -137,6 +137,7 @@ "electron" ], "overrides": { + "@ai-sdk/anthropic": "3.0.111", "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", @@ -145,7 +146,6 @@ }, "patchedDependencies": { "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", - "@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch", "@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", @@ -160,6 +160,8 @@ "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch", - "@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch" + "@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch", + "@ai-sdk/anthropic@3.0.111": "patches/@ai-sdk%2Fanthropic@3.0.111.patch", + "@ai-sdk/amazon-bedrock@4.0.166": "patches/@ai-sdk%2Famazon-bedrock@4.0.166.patch" } } diff --git a/packages/app/AGENTS.md b/packages/app/AGENTS.md index 72a973ebd4..5638ea8e6d 100644 --- a/packages/app/AGENTS.md +++ b/packages/app/AGENTS.md @@ -11,7 +11,7 @@ - `opencode dev web` proxies `https://app.opencode.ai`, so local UI/CSS changes will not show there. - For local UI changes, run the backend and app dev servers separately. -- Backend (from `packages/opencode`): `bun run --conditions=browser ./src/index.ts serve --port 4096` +- Backend (from `packages/opencode`): `bun run ./src/index.ts serve --port 4096` - App (from `packages/app`): `bun dev -- --port 4444` - Open `http://localhost:4444` to verify UI changes (it targets the backend at `http://localhost:4096`). diff --git a/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts b/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts index 136232c211..29eb680c29 100644 --- a/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts +++ b/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts @@ -60,6 +60,39 @@ test("keeps the file-browser sidebar mounted when switching file tabs", async ({ await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(scrolled) }) +test("keeps previous file search results visible while the next search loads", async ({ page }) => { + const searchPending = Promise.withResolvers() + await setup(page, async ({ query }) => { + if (query === "file-0") return ["file-00.ts"] + if (query === "file-7") { + await searchPending.promise + return ["file-79.ts"] + } + return [] + }) + + await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`) + await expectSessionTitle(page, title) + + const panel = page.locator("#review-panel") + await panel.getByRole("button", { name: "Open file" }).click() + const filter = panel.getByRole("combobox", { name: "Filter files" }) + await filter.fill("file-0") + await expect(panel.getByRole("option", { name: "file-00.ts" })).toBeVisible() + + const nextSearch = page.waitForRequest((request) => { + const url = new URL(request.url()) + return url.pathname === "/find/file" && url.searchParams.get("query") === "file-7" + }) + await filter.fill("file-7") + await nextSearch + await expect(panel.getByRole("option", { name: "file-00.ts" })).toBeVisible() + + searchPending.resolve() + await expect(panel.getByRole("option", { name: "file-79.ts" })).toBeVisible() + await expect(panel.getByRole("option", { name: "file-00.ts" })).toBeHidden() +}) + type Probed = HTMLElement & { __e2eProbe?: string } async function writeProbe(page: Page) { @@ -74,7 +107,10 @@ async function readProbe(page: Page) { .evaluate((el) => (el as Probed).__e2eProbe) } -async function setup(page: Page) { +async function setup( + page: Page, + findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown | Promise, +) { await mockOpenCodeServer(page, { directory, project: { @@ -119,6 +155,7 @@ async function setup(page: Page) { })) }, fileContent: (path) => ({ type: "text", content: `contents:${path}` }), + findFiles, pageMessages: () => ({ items: [] }), }) diff --git a/packages/app/e2e/regression/session-rename.spec.ts b/packages/app/e2e/regression/session-rename.spec.ts new file mode 100644 index 0000000000..2cd97c24c1 --- /dev/null +++ b/packages/app/e2e/regression/session-rename.spec.ts @@ -0,0 +1,141 @@ +import { expect, test } from "@playwright/test" +import { fixture, pageMessages } from "../smoke/session-timeline.fixture" +import { mockOpenCodeServer } from "../utils/mock-server" + +test.beforeEach(async ({ page }) => { + const sessions = fixture.sessions.map((session) => ({ ...session })) + await mockOpenCodeServer(page, { + protocol: "v1", + sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + }) + await page.route(/\/session\/[^/]+(?:\?.*)?$/, async (route) => { + if (route.request().method() !== "PATCH") return route.fallback() + const id = new URL(route.request().url()).pathname.split("/").at(-1) + const session = sessions.find((item) => item.id === id) + const payload: unknown = route.request().postDataJSON() + if ( + !session || + !payload || + typeof payload !== "object" || + !("title" in payload) || + typeof payload.title !== "string" + ) + throw new Error("Invalid rename request") + session.title = payload.title + await route.fulfill({ json: session, headers: { "access-control-allow-origin": "*" } }) + }) + await page.addInitScript((directory) => { + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: [{ worktree: directory, expanded: true }] }, + lastProject: { local: directory }, + }), + ) + }, fixture.directory) + await page.goto("/") + await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.targetTitle }).click() + await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible() +}) + +for (const commit of ["Enter", "blur", "click outside"]) { + test(`saves the session heading on ${commit}`, async ({ page }) => { + await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click() + const input = page.locator('input[data-slot="session-title-child"]') + await expect(input).toBeFocused() + await input.fill("Renamed session") + if (commit === "Enter") await input.press("Enter") + if (commit === "blur") await input.press("Tab") + if (commit === "click outside") await page.getByRole("textbox", { name: "Prompt", exact: true }).click() + await expect(page.getByRole("heading", { name: "Renamed session", exact: true })).toBeVisible() + await expect(page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Renamed session" })).toBeVisible() + await page.reload() + await expect(page.getByRole("heading", { name: "Renamed session", exact: true })).toBeVisible() + }) +} + +test("cancels the session heading with Escape", async ({ page }) => { + await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click() + const input = page.locator('input[data-slot="session-title-child"]') + await input.fill("Discard this title") + await input.press("Escape") + await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible() + await page.reload() + await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible() +}) + +test("keeps the draft when saving the session heading fails", async ({ page }) => { + await page.route(/\/session\/[^/]+(?:\?.*)?$/, (route) => { + if (route.request().method() !== "PATCH") return route.fallback() + return route.fulfill({ status: 500, headers: { "access-control-allow-origin": "*" } }) + }) + await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click() + const input = page.locator('input[data-slot="session-title-child"]') + await input.fill("Retry this title") + await input.press("Tab") + await expect(page.getByText("Request failed", { exact: true })).toBeVisible() + await expect(input).toBeEnabled() + await expect(input).toHaveValue("Retry this title") + await expect( + page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle }), + ).toBeVisible() +}) + +test("does not save an empty session heading", async ({ page }) => { + await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click() + const input = page.locator('input[data-slot="session-title-child"]') + await input.fill(" ") + await input.press("Tab") + await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible() + await page.reload() + await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible() +}) + +test("renames and closes the session tab from its context menu", async ({ page }) => { + const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle }) + await tab.click({ button: "right" }) + await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeVisible() + await page.keyboard.press("Escape") + await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeHidden() + await expect(tab).toBeFocused() + await tab.press("Shift+F10") + await page.getByRole("menuitem", { name: "Rename", exact: true }).click() + const input = page.locator('[data-slot="tab-title"][contenteditable="true"]') + await expect(input).toBeFocused() + await input.fill("Renamed from tab") + await input.press("Enter") + await expect(page.getByRole("heading", { name: "Renamed from tab", exact: true })).toBeVisible() + await page.reload() + await expect(page.getByRole("heading", { name: "Renamed from tab", exact: true })).toBeVisible() + const renamed = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Renamed from tab" }) + await renamed.click({ button: "right" }) + await page.getByRole("menuitem", { name: "Close tab", exact: true }).click() + await expect(renamed).toBeHidden() + await page.getByRole("button", { name: "Home", exact: true }).click() + await expect( + page.locator('[data-component="home-session-row"]').filter({ hasText: "Renamed from tab" }), + ).toBeVisible() +}) + +test("renames an inactive tab without switching sessions", async ({ page }) => { + await page.getByRole("button", { name: "Home", exact: true }).click() + await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.sourceTitle }).click() + await expect(page.getByRole("heading", { name: fixture.expected.sourceTitle, exact: true })).toBeVisible() + const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle }) + await tab.click({ button: "right" }) + await page.getByRole("menuitem", { name: "Rename", exact: true }).click() + const input = page.locator('[data-slot="tab-title"][contenteditable="true"]') + await expect(input).toBeFocused() + await input.fill("Inactive tab renamed") + await input.press("Tab") + await expect(page.getByRole("heading", { name: fixture.expected.sourceTitle, exact: true })).toBeVisible() + await expect(page).toHaveURL(new RegExp(`/session/${fixture.sourceID}$`)) + await page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Inactive tab renamed" }).click() + await expect(page.getByRole("heading", { name: "Inactive tab renamed", exact: true })).toBeVisible() + await page.reload() + await expect(page.getByRole("heading", { name: "Inactive tab renamed", exact: true })).toBeVisible() +}) diff --git a/packages/app/e2e/regression/subagent-child-navigation.spec.ts b/packages/app/e2e/regression/subagent-child-navigation.spec.ts index 019cc156ec..c4ed6a78a0 100644 --- a/packages/app/e2e/regression/subagent-child-navigation.spec.ts +++ b/packages/app/e2e/regression/subagent-child-navigation.spec.ts @@ -39,7 +39,7 @@ test("shows the not found fallback when the viewed session is deleted", async ({ }) await expect(page.getByText("This session cannot be found")).toBeVisible() - await expect(page.getByRole("button", { name: "Close Tab" })).toBeVisible() + await expect(page.getByRole("button", { name: "Close Tab", exact: true })).toBeVisible() await expect(page.getByRole("heading", { name: taskDescription })).toHaveCount(0) }) diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index 76987421b6..a581eda8c1 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -26,7 +26,7 @@ export interface MockServerConfig { questions?: unknown[] | (() => unknown[]) fileList?: (path: string) => unknown | Promise fileContent?: (path: string) => unknown | Promise - findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown + findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown | Promise sessionStatus?: Record | (() => Record) } diff --git a/packages/app/package.json b/packages/app/package.json index f31f65eba6..4accd2c499 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.18", + "version": "1.18.29", "description": "", "type": "module", "exports": { @@ -32,7 +32,7 @@ }, "license": "MIT", "devDependencies": { - "@happy-dom/global-registrator": "20.0.11", + "@happy-dom/global-registrator": "20.12.0", "@playwright/test": "catalog:", "@sentry/vite-plugin": "catalog:", "@tailwindcss/vite": "catalog:", diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index aa1f519984..1081310e5f 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -33,6 +33,7 @@ import { ExternalLink } from "@/components/external-link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" import { popularProviders, useProviders } from "@/hooks/use-providers" import { CustomProviderForm } from "./dialog-custom-provider" @@ -386,6 +387,7 @@ function ProviderConnection(props: { const serverSDK = useServerSDK() const params = useParams() const language = useLanguage() + const platform = usePlatform() const settings = useSettings() const newLayout = settings.general.newLayoutDesigns const providers = useProviders(() => props.directory?.()) @@ -560,6 +562,11 @@ function ProviderConnection(props: { }) .then((x) => { if (!alive.value) return + if (props.provider === "opencode" && platform.platform === "desktop") { + const url = new URL(x.data.url) + url.searchParams.set("client_id", "opencode-desktop") + x.data.url = url.href + } dispatch({ type: "auth.complete", authorization: x.data }) }) .catch((e) => { diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 9066f72434..587a36a666 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -450,7 +450,7 @@ function ModelSelectorPopoverV2View(props: { {(group) => ( - + {group.items[0].provider.name} diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index aa16976228..102c1a7cd1 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -145,7 +145,7 @@ function ServerForm(props: ServerFormProps) { type="text" label={language.t("dialog.server.add.name")} placeholder={language.t("dialog.server.add.namePlaceholder")} - value={props.name} + defaultValue={props.name} disabled={props.busy} onChange={props.onNameChange} onKeyDown={keyDown} @@ -155,7 +155,7 @@ function ServerForm(props: ServerFormProps) { type="text" label={language.t("dialog.server.add.username")} placeholder={language.t("dialog.server.add.usernamePlaceholder")} - value={props.username} + defaultValue={props.username} disabled={props.busy} onChange={props.onUsernameChange} onKeyDown={keyDown} @@ -164,7 +164,7 @@ function ServerForm(props: ServerFormProps) { type="password" label={language.t("dialog.server.add.password")} placeholder={language.t("dialog.server.add.passwordPlaceholder")} - value={props.password} + defaultValue={props.password} disabled={props.busy} onChange={props.onPasswordChange} onKeyDown={keyDown} diff --git a/packages/app/src/components/opencodez-prompt-control.tsx b/packages/app/src/components/opencodez-prompt-control.tsx index bd392b283a..649e21c0f2 100644 --- a/packages/app/src/components/opencodez-prompt-control.tsx +++ b/packages/app/src/components/opencodez-prompt-control.tsx @@ -5,6 +5,7 @@ import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" +import { showToast } from "@/utils/toast" type Options = { sessionID: Accessor @@ -47,6 +48,7 @@ export function createOpenCodezPromptControl(options: Options): { let trigger: HTMLButtonElement | undefined let menu: HTMLDivElement | undefined let request = 0 + let refreshError: string | undefined const refresh = async () => { const current = ++request @@ -57,11 +59,20 @@ export function createOpenCodezPromptControl(options: Options): { metadata: sessionID ? undefined : state.metadata, model: options.model(), }) - .then((value) => value.data ?? undefined) - .catch(() => undefined) - if (!result || current !== request) return - setState("prompt", result.state) - if (!sessionID) setState("metadata", result.metadata) + .then((value) => ({ data: value.data ?? undefined })) + .catch((error: unknown) => ({ error: error instanceof Error ? error.message : String(error) })) + if (current !== request) return + if ("error" in result || !result.data) { + const description = "error" in result ? result.error : language.t("common.requestFailed") + setState("error", description) + if (refreshError !== description) showToast({ title: language.t("common.requestFailed"), description }) + refreshError = description + return + } + refreshError = undefined + setState("error", undefined) + setState("prompt", result.data.state) + if (!sessionID) setState("metadata", result.data.metadata) } createEffect( @@ -91,14 +102,16 @@ export function createOpenCodezPromptControl(options: Options): { setState({ loading: true, error: undefined }) const entries = await sdk() .client.opencodez.prompt.list() - .then((value) => value.data ?? undefined) - .catch(() => undefined) + .then((value) => ({ data: value.data ?? undefined })) + .catch((error: unknown) => ({ error: error instanceof Error ? error.message : String(error) })) setState("loading", false) - if (!entries) { - setState("error", language.t("common.requestFailed")) + if ("error" in entries || !entries.data) { + const description = "error" in entries ? entries.error : language.t("common.requestFailed") + setState("error", description) + showToast({ title: language.t("common.requestFailed"), description }) return } - setState({ entries, loaded: true }) + setState({ entries: entries.data, loaded: true }) } const position = () => { @@ -160,11 +173,16 @@ export function createOpenCodezPromptControl(options: Options): { model: options.model(), name, }) - .then((value) => value.data ?? undefined) - .catch(() => undefined) - if (!result) return - setState("prompt", result.state) - if (!sessionID) setState("metadata", result.metadata) + .then((value) => ({ data: value.data ?? undefined })) + .catch((error: unknown) => ({ error: error instanceof Error ? error.message : String(error) })) + if ("error" in result || !result.data) { + const description = "error" in result ? result.error : language.t("common.requestFailed") + showToast({ title: language.t("common.requestFailed"), description }) + await refresh() + return + } + setState("prompt", result.data.state) + if (!sessionID) setState("metadata", result.data.metadata) setOpen(false) } diff --git a/packages/app/src/components/session/open-in-app-v2.tsx b/packages/app/src/components/session/open-in-app-v2.tsx index e26ff2e0ea..b5291dcc32 100644 --- a/packages/app/src/components/session/open-in-app-v2.tsx +++ b/packages/app/src/components/session/open-in-app-v2.tsx @@ -31,7 +31,10 @@ export function OpenInAppV2(props: { directory: () => string }) { disabled={state.opening()} aria-label={language.t("session.header.open.ariaLabel", { app: state.current().label })} > - }> + } + > diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index 65493c54df..b016f286a2 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -1,9 +1,11 @@ import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } from "solid-js" +import { createStore } from "solid-js/store" import { makeEventListener } from "@solid-primitives/event-listener" import { createResizeObserver } from "@solid-primitives/resize-observer" import { createMutation } from "@tanstack/solid-query" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import { useGlobal } from "@/context/global" import { useLanguage } from "@/context/language" import { ServerConnection, serverName } from "@/context/server" @@ -33,6 +35,8 @@ export function TabNavItem(props: { pressed?: boolean hidden?: boolean }) { + const language = useLanguage() + const [menu, setMenu] = createStore({ open: false, rename: false }) const [editing, setEditing] = createSignal(false) const [titleOverflowing, setTitleOverflowing] = createSignal(false) let tabRoot!: HTMLDivElement @@ -76,7 +80,7 @@ export function TabNavItem(props: { }) const [popoverOpen, setPopoverOpen] = createSignal(false) - const previewBlocked = () => !!props.dragging || editing() || !!props.pressed || !props.session() + const previewBlocked = () => !!props.dragging || editing() || menu.open || !!props.pressed || !props.session() const measureTitleOverflow = () => { if (!titleEl || editing()) { @@ -138,9 +142,9 @@ export function TabNavItem(props: { titleEl.textContent = value }) - const openRename = (event: MouseEvent) => { - event.preventDefault() - event.stopPropagation() + const openRename = (event?: MouseEvent) => { + event?.preventDefault() + event?.stopPropagation() if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return const session = props.session() if (!session) return @@ -171,7 +175,7 @@ export function TabNavItem(props: { onCleanup(cleanup) }) - const tab = ( + const tab = () => (
{ tabRoot = el @@ -196,7 +200,11 @@ export function TabNavItem(props: { closeTab(event) }} > - - +
} + aria-label={language.t("common.closeTab")} />
) return ( - { - if (value && previewBlocked()) return - setPopoverOpen(value) - }} - data={{ - projectName: projectName(), - title: props.session()?.title, - path: previewPath(), - serverName: serverLabel(), + { + setMenu("open", open) + if (open) setPopoverOpen(false) }} - /> + > + { + if (value && previewBlocked()) return + setPopoverOpen(value) + }} + data={{ + projectName: projectName(), + title: props.session()?.title, + path: previewPath(), + serverName: serverLabel(), + }} + /> + + { + if (!menu.rename) return + event.preventDefault() + setMenu("rename", false) + openRename() + }} + > + setMenu("rename", true)}> + {language.t("common.rename")} + + {language.t("common.closeTab")} + + + ) } diff --git a/packages/app/src/context/global-sync/home-session-index.test.ts b/packages/app/src/context/global-sync/home-session-index.test.ts index 4e40cc78ea..9b94f1de21 100644 --- a/packages/app/src/context/global-sync/home-session-index.test.ts +++ b/packages/app/src/context/global-sync/home-session-index.test.ts @@ -1,8 +1,10 @@ import { describe, expect, test } from "bun:test" -import type { SessionV2Info } from "@opencode-ai/sdk/v2/client" +import { QueryClient } from "@tanstack/solid-query" +import type { Session, SessionV2Info } from "@opencode-ai/sdk/v2/client" import { applyHomeSessionEvent, appendHomeSessionEvent, + createHomeSessionIndexCache, HOME_V2_SESSION_PAGE_LIMIT, loadHomeSessionIndex, homeSessionIndexSessions, @@ -151,4 +153,33 @@ describe("Home V2 session index", () => { expect(homeSessionIndexRefresh("global.disposed", true).refetch).toBe(true) expect(homeSessionIndexRefresh("session.next.moved", true).refetch).toBe(true) }) + + test("removes a session from the loaded Home index", () => { + const queryClient = new QueryClient() + const cache = createHomeSessionIndexCache(queryClient, "server") + const sessions = [ + { id: "a", time: { created: 1, updated: 1 } }, + { id: "b", time: { created: 1, updated: 1 } }, + ] as Session[] + queryClient.setQueryData(cache.indexKey, { sessions, eventSequence: 0 }) + + cache.remove("a") + + const index = queryClient.getQueryData<{ sessions: Session[] }>(cache.indexKey) + expect(index?.sessions.map((item) => item.id)).toEqual(["b"]) + }) + + test("keeps the session out of the Home list when the index is not mounted", () => { + const queryClient = new QueryClient() + const cache = createHomeSessionIndexCache(queryClient, "server") + const sessions = [ + { id: "a", time: { created: 1, updated: 1 } }, + { id: "b", time: { created: 1, updated: 1 } }, + ] as Session[] + + cache.remove("a") + + expect(queryClient.getQueryData(cache.indexKey)).toBeUndefined() + expect(cache.sessions({ sessions, eventSequence: 0 }, undefined).map((item) => item.id)).toEqual(["b"]) + }) }) diff --git a/packages/app/src/context/global-sync/home-session-index.ts b/packages/app/src/context/global-sync/home-session-index.ts index 03a085e34d..781c39c450 100644 --- a/packages/app/src/context/global-sync/home-session-index.ts +++ b/packages/app/src/context/global-sync/home-session-index.ts @@ -85,6 +85,7 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st const indexKey = homeSessionIndexKey(server) const eventsKey = homeSessionEventsKey(server) let connected = false + const removed = new Set() return { indexKey, @@ -97,7 +98,8 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st queryClient.setQueryData(eventsKey, (current) => trimHomeSessionEvents(current, sequence)) }, sessions(index: HomeSessionIndex | undefined, events: HomeSessionEvents | undefined) { - return homeSessionIndexSessions(index, events) + const sessions = homeSessionIndexSessions(index, events) + return removed.size === 0 ? sessions : sessions.filter((session) => !removed.has(session.id)) }, apply(event: HomeSessionEvent) { if (!queryClient.getQueryState(indexKey)) return @@ -116,6 +118,16 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st } queryClient.setQueryData(eventsKey, { sequence: next.sequence, entries: [] }) }, + remove(sessionID: string) { + removed.add(sessionID) + if (!queryClient.getQueryState(indexKey)) return + queryClient.setQueryData(indexKey, (index) => { + if (!index) return index + const at = index.sessions.findIndex((session) => session.id === sessionID) + if (at === -1) return index + return { ...index, sessions: index.sessions.toSpliced(at, 1) } + }) + }, refresh(event: Event["type"]) { const result = homeSessionIndexRefresh(event, connected) connected = result.connected diff --git a/packages/app/src/pages/home/home-sessions-controller.tsx b/packages/app/src/pages/home/home-sessions-controller.tsx index 25d896393c..f306f208cc 100644 --- a/packages/app/src/pages/home/home-sessions-controller.tsx +++ b/packages/app/src/pages/home/home-sessions-controller.tsx @@ -219,13 +219,15 @@ export function createHomeSessionsController(home: HomeController) { directory: session.directory, time: { archived: Date.now() }, }), - remove: () => + remove: () => { setStore( produce((draft) => { const match = Binary.search(draft.session, session.id, (item) => item.id) if (match.found) draft.session.splice(match.index, 1) }), - ), + ) + homeSessions().remove(session.id) + }, onError: (cause) => showToast({ title: language.t("common.requestFailed"), diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 95f8c01843..98548ab233 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -972,17 +972,6 @@ export default function LegacyLayout(props: ParentProps) { keybind: "shift+alt+arrowdown", onSelect: () => navigateSessionByUnseen(1), }, - { - id: "session.archive", - title: language.t("command.session.archive"), - category: language.t("command.category.session"), - keybind: "mod+shift+backspace", - disabled: !params.dir || !params.id, - onSelect: () => { - const session = currentSessions().find((s) => s.id === params.id) - if (session) void archiveSession(session) - }, - }, { id: "workspace.new", title: language.t("workspace.new"), diff --git a/packages/app/src/pages/session/session-archive.ts b/packages/app/src/pages/session/session-archive.ts new file mode 100644 index 0000000000..3968869539 --- /dev/null +++ b/packages/app/src/pages/session/session-archive.ts @@ -0,0 +1,74 @@ +import { useNavigate } from "@solidjs/router" +import { produce } from "solid-js/store" +import { notifySessionTabsRemoved } from "@/components/titlebar-session-events" +import { useLanguage } from "@/context/language" +import { useSDK } from "@/context/sdk" +import { useServerSync } from "@/context/server-sync" +import { useSync } from "@/context/sync" +import { useTabs } from "@/context/tabs" +import { errorMessage } from "@/pages/layout/helpers" +import { useSessionKey } from "@/pages/session/session-layout" +import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route" +import { showToast } from "@/utils/toast" + +export function useSessionArchive() { + const language = useLanguage() + const navigate = useNavigate() + const sdk = useSDK() + const sync = useSync() + const serverSync = useServerSync() + const tabs = useTabs() + const { params } = useSessionKey() + + const navigateAfterRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => { + if (params.id !== sessionID) return + const href = (id: string) => + params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id) + if (parentID) { + navigate(href(parentID)) + return + } + if (nextSessionID) { + navigate(href(nextSessionID)) + return + } + if (params.serverKey) { + tabs.newDraft({ server: requireServerKey(params.serverKey), directory: sdk().directory }) + return + } + navigate(`/${params.dir}/session`) + } + + const archive = async (sessionID: string) => { + const session = sync().session.get(sessionID) + if (!session) return + if ((await sdk().protocol) !== "v1") return + + const sessions = sync().data.session ?? [] + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + await sdk() + .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } }) + .then(() => { + sync().set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === sessionID) + if (index !== -1) draft.session.splice(index, 1) + }), + ) + sync().session.evict(sessionID) + serverSync().homeSessions.remove(sessionID) + navigateAfterRemoval(sessionID, session.parentID, nextSession?.id) + notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] }) + }) + .catch((err) => { + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err, language.t("common.requestFailed")), + }) + }) + } + + return { archive, navigateAfterRemoval } +} diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 25abdc9e73..e083882557 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -63,10 +63,10 @@ import { SessionContextUsage } from "@/components/session-context-usage" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useLanguage } from "@/context/language" import { useSessionKey } from "@/pages/session/session-layout" +import { useSessionArchive } from "@/pages/session/session-archive" import { useServerSDK } from "@/context/server-sdk" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" -import { useTabs } from "@/context/tabs" import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" @@ -263,8 +263,8 @@ export function MessageTimeline(props: { const sdk = useSDK() const sync = useSync() const settings = useSettings() - const tabs = useTabs() const dialog = useDialog() + const sessionArchive = useSessionArchive() const language = useLanguage() const { params, sessionKey } = useSessionKey() const ownerSessionKey = sessionKey() @@ -777,6 +777,7 @@ export function MessageTimeline(props: { } const saveTitleEditor = () => { + if (!title.editing) return const id = sessionID() if (!id) return if (titleMutation.isPending) return @@ -790,25 +791,6 @@ export function MessageTimeline(props: { titleMutation.mutate({ id, title: next }) } - const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => { - if (params.id !== sessionID) return - const href = (id: string) => - params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id) - if (parentID) { - navigate(href(parentID)) - return - } - if (nextSessionID) { - navigate(href(nextSessionID)) - return - } - if (params.serverKey) { - tabs.newDraft({ server: requireServerKey(params.serverKey), directory: sdk().directory }) - return - } - navigate(`/${params.dir}/session`) - } - const exportSession = async (sessionID: string) => { try { const data = await fetchSessionExport({ @@ -832,36 +814,6 @@ export function MessageTimeline(props: { } } - const archiveSession = async (sessionID: string) => { - const session = sync().session.get(sessionID) - if (!session) return - if ((await sdk().protocol) !== "v1") return - - const sessions = sync().data.session ?? [] - const index = sessions.findIndex((s) => s.id === sessionID) - const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) - - await sdk() - .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } }) - .then(() => { - sync().set( - produce((draft) => { - const index = draft.session.findIndex((s) => s.id === sessionID) - if (index !== -1) draft.session.splice(index, 1) - }), - ) - sync().session.evict(sessionID) - navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) - notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] }) - }) - .catch((err) => { - showToast({ - title: language.t("common.requestFailed"), - description: errorMessage(err), - }) - }) - } - const deleteSession = async (sessionID: string) => { const session = sync().session.get(sessionID) if (!session) return false @@ -911,7 +863,7 @@ export function MessageTimeline(props: { } } - navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) + sessionArchive.navigateAfterRemoval(sessionID, session.parentID, nextSession?.id) sync().set( produce((draft) => { @@ -1496,6 +1448,7 @@ export function MessageTimeline(props: { onInput={(event) => setTitle("draft", event.currentTarget.value)} onKeyDown={(event) => { event.stopPropagation() + if (event.isComposing || event.keyCode === 229) return if (event.key === "Enter") { event.preventDefault() void saveTitleEditor() @@ -1506,7 +1459,7 @@ export function MessageTimeline(props: { closeTitleEditor() } }} - onBlur={closeTitleEditor} + onBlur={saveTitleEditor} />
@@ -1593,7 +1546,7 @@ export function MessageTimeline(props: { exportSession(id)}> {language.t("common.export")} - void archiveSession(id)}> + void sessionArchive.archive(id)}> {language.t("common.archive")} @@ -1667,7 +1620,7 @@ export function MessageTimeline(props: { exportSession(id)}> {language.t("common.export")}... - void archiveSession(id)}> + void sessionArchive.archive(id)}> {language.t("common.archive")} diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index c4c8c44a99..18d5b172ff 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -18,6 +18,7 @@ import { createSessionTabs } from "@/pages/session/helpers" import { extractPromptFromParts } from "@/utils/prompt" import { Message, Part, UserMessage } from "@opencode-ai/sdk/v2" import { useSessionLayout } from "@/pages/session/session-layout" +import { useSessionArchive } from "@/pages/session/session-archive" import { createSessionOwnership } from "./session-ownership" import { useLocal } from "@/context/local" @@ -52,6 +53,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const navigate = useNavigate() const { params, sessionKey, tabs, view } = useSessionLayout() const sessionOwnership = createSessionOwnership(sessionKey) + const sessionArchive = useSessionArchive() const openDialog = async (load: () => Promise, show: (value: T) => void) => { const owner = sessionOwnership.capture() const value = await load() @@ -497,6 +499,16 @@ export const useSessionCommands = (actions: SessionCommandContext) => { disabled: !params.id, onSelect: exportSession, }), + sessionCommand({ + id: "session.archive", + title: language.t("command.session.archive"), + keybind: "mod+shift+backspace", + disabled: !params.id, + onSelect: () => { + const id = params.id + if (id) void sessionArchive.archive(id) + }, + }), ] const fileCmds = () => { diff --git a/packages/app/src/pages/session/v2/session-file-browser-tab.tsx b/packages/app/src/pages/session/v2/session-file-browser-tab.tsx index 639429e80b..c9e0d38167 100644 --- a/packages/app/src/pages/session/v2/session-file-browser-tab.tsx +++ b/packages/app/src/pages/session/v2/session-file-browser-tab.tsx @@ -1,5 +1,5 @@ import { createMemo, createSignal, createUniqueId, Show } from "solid-js" -import { createQuery } from "@tanstack/solid-query" +import { createQuery, keepPreviousData } from "@tanstack/solid-query" import { Icon } from "@opencode-ai/ui/icon" import { SessionFilePanelV2, SessionFilePanelV2Empty } from "@opencode-ai/session-ui/v2/session-file-panel-v2" import { SessionReviewV2Sidebar } from "@opencode-ai/session-ui/v2/session-review-v2" @@ -50,6 +50,7 @@ export function SessionFileBrowserTab(props: { queryKey: ["session-open-file", workspaceKey(), value] as const, enabled: value.length > 0, queryFn: ({ signal }) => file.searchFiles(value, { limit: 200, signal }), + placeholderData: keepPreviousData, } }) const files = createMemo(() => { diff --git a/packages/app/src/utils/draft-store.ts b/packages/app/src/utils/draft-store.ts index cd0895f52c..30eee8fd4a 100644 --- a/packages/app/src/utils/draft-store.ts +++ b/packages/app/src/utils/draft-store.ts @@ -22,7 +22,14 @@ function blobUrl(id: string, blob: Blob) { } async function blobID(blob: Blob) { - const id = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await blob.arrayBuffer()))) + const subtle = globalThis.crypto?.subtle + if (!subtle) { + // Browsers withhold SubtleCrypto on non-loopback HTTP origins but retain getRandomValues. + const bytes = globalThis.crypto?.getRandomValues(new Uint8Array(16)) + if (!bytes) return `random-${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}` + return `random-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}` + } + const id = Array.from(new Uint8Array(await subtle.digest("SHA-256", await blob.arrayBuffer()))) .map((byte) => byte.toString(16).padStart(2, "0")) .join("") return id diff --git a/packages/cli/package.json b/packages/cli/package.json index 5b9e5aa40a..dc70e71ef4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.18.18", + "version": "1.18.29", "type": "module", "license": "MIT", "bin": { diff --git a/packages/codemode/package.json b/packages/codemode/package.json index a093c82d98..66f983ad82 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.18.18", + "version": "1.18.29", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 3d90f1c7b5..f5d06e24d7 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.18.18", + "version": "1.18.29", "type": "module", "license": "MIT", "scripts": { @@ -29,6 +29,7 @@ "@upstash/redis": "1.38.0", "chart.js": "4.5.1", "nitro": "3.0.1-alpha.1", + "number-flow": "0.6.2", "solid-js": "catalog:", "solid-list": "0.3.0", "solid-stripe": "0.8.1", diff --git a/packages/console/app/src/component/limits-graph.tsx b/packages/console/app/src/component/limits-graph.tsx new file mode 100644 index 0000000000..63ffeafaa7 --- /dev/null +++ b/packages/console/app/src/component/limits-graph.tsx @@ -0,0 +1,268 @@ +import { For, createSignal, onCleanup, onMount } from "solid-js" +import { useI18n } from "~/context/i18n" +import { RollingNumber } from "./rolling-number" + +export function LimitsGraph(props: { href: string }) { + let root!: HTMLElement + const [visible, setVisible] = createSignal(false) + const [boosted, setBoosted] = createSignal(false) + const [promoted, setPromoted] = createSignal([]) + let timer: ReturnType | undefined + + const i18n = useI18n() + + onMount(() => { + const motion = window.matchMedia("(prefers-reduced-motion: reduce)") + const finish = () => { + if (!motion.matches) return + clearTimeout(timer) + setVisible(true) + setBoosted(true) + setPromoted(bonuses.map((model) => model.id)) + } + motion.addEventListener("change", finish) + onCleanup(() => { + clearTimeout(timer) + motion.removeEventListener("change", finish) + }) + if (motion.matches) return finish() + if (typeof IntersectionObserver === "undefined") return setVisible(true) + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0] + if (!entry?.isIntersecting || entry.intersectionRatio < 0.35) return + setVisible(true) + observer.disconnect() + }, + { threshold: 0.35 }, + ) + observer.observe(root) + onCleanup(() => observer.disconnect()) + }) + + const baseline = 100 + const graph = [ + { id: "kimi-k3", name: "Kimi K3", req: 110 }, + { id: "grok-4.6", name: "Grok 4.6", req: 169 }, + { id: "hy4-preview", name: "Hy4 preview", req: 1350 }, + { id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 2050 }, + { id: "glm-5.3-flash", name: "GLM-5.3-Flash", req: 3160, baseReq: 1580, bonus: "2x usage" }, + { id: "minimax-m3", name: "MiniMax M3", req: 3200 }, + { id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300 }, + { id: "hy3", name: "Hy3", req: 4300 }, + { id: "qwen3.8-flash", name: "Qwen3.8 Flash", req: 5400 }, + { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 7600 }, + { id: "longcat-2.0", name: "LongCat-2.0", req: 11400 }, + { id: "omen-alpha", name: "Omen Alpha", req: 11600 }, + { id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100 }, + { id: "muse-spark-1.3-contributor", name: "Muse Spark 1.3 Contributor", req: 45300, edge: true }, + ].map((model, index) => ({ ...model, d: `${50 + index * 25}ms` })) + const bonuses = graph.filter((model) => model.baseReq) + + const w = 1040 + const chartW = 720 + const left = 40 + const right = 60 + const top = 18 + const bottom = 44 + const plot = chartW - left - right + const infiniteX = w - 180 + + const ratio = (n: number) => n / baseline + const rmax = Math.max(1, ...graph.filter((m) => !("infinite" in m)).map((m) => ratio(m.req))) + const log = (n: number) => Math.log10(Math.max(n, 1)) + const base = 24 + const p = 2.2 + const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base) + const ticks = [1, 5, 10, 25, 50, 100, 250].filter((t) => t <= rmax) + const labels = (() => { + const set = new Set() + let last = -Infinity + for (const t of ticks) { + if (t === 1) { + set.add(t) + last = x(t) + continue + } + const pos = x(t) + if (pos - last < 44) continue + set.add(t) + last = pos + } + return set + })() + const shown = ticks.filter((t) => labels.has(t)) + const bh = 8 + const gap = 20 + const step = bh + gap + const gy = (i: number) => top + 22 + step * i + const h = gy(graph.length - 1) + bottom + const my = graph.length < 2 ? gy(0) : (gy(0) + gy(graph.length - 1)) / 2 + const px = (n: number) => `${(n / w) * 100}%` + const py = (n: number) => `${(n / h) * 100}%` + const lx = px(left - 16) + const ty = py(h - 18) + const timing = () => { + const style = getComputedStyle(root) + return { + duration: Number.parseFloat(style.getPropertyValue("--bonus-duration")), + easing: style.getPropertyValue("--spring-easing").trim(), + spinEasing: style.getPropertyValue("--digit-easing").trim(), + } + } + + return ( +
{ + if (!(event.target instanceof SVGElement) || event.animationName !== "go-graph-reveal") return + if (event.target.hasAttribute("data-stage-end") && !boosted()) { + const duration = Number.parseFloat(getComputedStyle(root).getPropertyValue("--reveal-duration")) + timer = setTimeout(() => setBoosted(true), duration * 0.6) + return + } + if (event.target.dataset.animate !== "bonus") return + const model = event.target.dataset.model + if (!model) return + setPromoted((current) => [...current, model]) + }} + > +
+ + + + + + +
+ + {(m, i) => ( + + + {!("infinite" in m) && m.baseReq ? ( + + ) : ( + {"infinite" in m ? "\u221e" : m.req.toLocaleString()} + )} + {m.name} + {m.id === "muse-spark-1.3-contributor" && ( + + ( + + {i18n.t("go.graph.limitedRegions")} + + ) + + )} + {"infinite" in m && ({i18n.t("go.graph.limitedTime")})} + + {"bonus" in m && {m.bonus}} + + )} + +
+
+ +
+
+
+
+ {i18n.t("go.graph.label")} + + {i18n.t("go.graph.usageLimits")} + +
+
+
+
+
+ ) +} diff --git a/packages/console/app/src/component/rolling-number.tsx b/packages/console/app/src/component/rolling-number.tsx new file mode 100644 index 0000000000..df1af6ca20 --- /dev/null +++ b/packages/console/app/src/component/rolling-number.tsx @@ -0,0 +1,58 @@ +import NumberFlow from "number-flow" +import { continuous } from "number-flow/plugins" +import { createEffect, onCleanup, onMount } from "solid-js" + +export function RollingNumber(props: { + value: number + target: number + timing: () => { duration: number; easing: string; spinEasing: string } +}) { + let root!: HTMLSpanElement + // Keep the server-rendered text stable while the custom element owns its contents. + const initial = props.value.toLocaleString() + const growing = props.target.toLocaleString().length > initial.length + + onMount(() => { + if (new Intl.NumberFormat().resolvedOptions().numberingSystem !== "latn") { + createEffect(() => { + root.textContent = props.value.toLocaleString() + }) + return + } + + const flow = new NumberFlow() + flow.format = { useGrouping: true, maximumFractionDigits: 0 } + flow.trend = 1 + if (growing) flow.plugins = [continuous] + flow.opacityTiming = { duration: 180, easing: "ease-out" } + const motion = window.matchMedia("(prefers-reduced-motion: reduce)") + const updateMotion = () => { + flow.animated = !motion.matches + } + updateMotion() + motion.addEventListener("change", updateMotion) + onCleanup(() => motion.removeEventListener("change", updateMotion)) + root.replaceChildren(flow) + createEffect(() => { + const value = props.value + if (flow.value === value) return + const timing = props.timing() + flow.transformTiming = { duration: timing.duration, easing: timing.easing } + flow.spinTiming = { + duration: timing.duration, + easing: growing ? "cubic-bezier(0.45, 0, 0.55, 1)" : timing.spinEasing, + } + flow.update(value) + }) + }) + + return ( + + {initial} + + ) +} diff --git a/packages/console/app/src/context/auth.ts b/packages/console/app/src/context/auth.ts index aed07a630f..90669d7647 100644 --- a/packages/console/app/src/context/auth.ts +++ b/packages/console/app/src/context/auth.ts @@ -1,6 +1,7 @@ import { getRequestEvent } from "solid-js/web" import { and, Database, eq, inArray, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js" import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { redirect } from "@solidjs/router" import { Actor } from "@opencode-ai/console-core/actor.js" @@ -79,8 +80,15 @@ export const getActor = async (workspace?: string): Promise => { if (accounts.length) { const user = await Database.use((tx) => tx - .select() + .select({ + id: UserTable.id, + workspaceID: UserTable.workspaceID, + accountID: UserTable.accountID, + role: UserTable.role, + migratedAt: WorkspaceTable.migrated_at, + }) .from(UserTable) + .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID)) .where( and( eq(UserTable.workspaceID, workspace), @@ -93,6 +101,15 @@ export const getActor = async (workspace?: string): Promise => { .then((x) => x[0]), ) if (user) { + if (user.migratedAt) { + const destination = Resource.ConsoleMigration.consoleUrl + if (!destination) throw new Error("New Console URL is not configured") + evt.response.headers.set("Cache-Control", "no-store") + throw redirect(`${destination}/login`, { + status: evt.request.method === "GET" || evt.request.method === "HEAD" ? 302 : 303, + headers: { "Cache-Control": "no-store" }, + }) + } await Database.use((tx) => tx .update(UserTable) diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 887fee9ec8..fb7dae2a84 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -252,9 +252,8 @@ export const dict = { "zen.privacy.exceptionsLink": "الاستثناءات التالية", "go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع", - "go.banner.text": "يحصل DeepSeek V4 Flash على حدود استخدام مضاعفة لفترة محدودة", - "go.meta.description": - "يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.", + "go.banner.text": "يحصل GLM-5.3-Flash على حدود استخدام مضاعفة لفترة محدودة", + "go.meta.description": "يبلغ سعر Go ‏$10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.", "go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع", "go.hero.body": "يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.", @@ -263,13 +262,13 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "اشترك في Go", "go.cta.price": "$10/شهر", - "go.cta.promo": "$5 للشهر الأول", - "go.pricing.body": - "استخدمه مع أي وكيل. $5 للشهر الأول، ثم $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.", + "go.pricing.body": "استخدمه مع أي وكيل. $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.", "go.graph.free": "مجاني", "go.graph.freePill": "Big Pickle ونماذج مجانية", "go.graph.go": "Go", "go.graph.label": "الطلبات كل 5 ساعات", + "go.graph.limitedRegions": "مناطق محدودة", + "go.graph.limitedTime": "لفترة محدودة", "go.graph.usageLimits": "حدود الاستخدام", "go.graph.aria": "الطلبات كل 5 ساعات: {{free}} مقابل {{go}}", @@ -296,20 +295,20 @@ export const dict = { "go.testimonials.frank.quote": "أتمنى لو كنت لا أزال في Nvidia.", "go.problem.title": "ما المشكلة التي يحلها Go؟", "go.problem.body": - "نحن نركز على تقديم تجربة OpenCode لأكبر عدد ممكن من الناس. OpenCode Go هو اشتراك منخفض التكلفة: $5 للشهر الأول، ثم $10/شهر. يوفر حدودا سخية ووصولا موثوقا إلى نماذج المصدر المفتوح الأكثر قدرة.", + "نحن نركز على تقديم تجربة OpenCode لأكبر عدد ممكن من الناس. OpenCode Go هو اشتراك منخفض التكلفة بسعر $10/شهر. يوفر حدودا سخية ووصولا موثوقا إلى نماذج المصدر المفتوح الأكثر قدرة.", "go.problem.subtitle": " ", "go.problem.item1": "أسعار اشتراك منخفضة التكلفة", "go.problem.item2": "حدود سخية ووصول موثوق", "go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين", "go.problem.item4": "مجموعة منسقة من النماذج المختبرة للبرمجة الوكيلة", "go.how.title": "كيف يعمل Go", - "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", + "go.how.body": "يبلغ سعر Go ‏$10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", "go.how.step1.title": "أنشئ حسابًا", "go.how.step1.beforeLink": "اتبع", "go.how.step1.link": "تعليمات الإعداد", "go.how.step2.title": "اشترك في Go", - "go.how.step2.link": "$5 للشهر الأول", - "go.how.step2.afterLink": "ثم $10/شهر مع حدود سخية", + "go.how.step2.link": "$10/شهر", + "go.how.step2.afterLink": "مع حدود سخية", "go.how.step3.title": "ابدأ البرمجة", "go.how.step3.body": "مع وصول موثوق لنماذج مفتوحة المصدر", "go.privacy.title": "خصوصيتك مهمة بالنسبة لنا", @@ -325,11 +324,11 @@ export const dict = { "go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.", "go.faq.q3": "هل Go هو نفسه Zen؟", "go.faq.a3": - "لا. يعتمد Zen على الدفع حسب الاستخدام، بينما يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى مجموعة منسقة من النماذج.", + "لا. يعتمد Zen على الدفع حسب الاستخدام، بينما يبلغ سعر Go ‏$10/شهر، مع حدود سخية ووصول موثوق إلى مجموعة منسقة من النماذج.", "go.faq.q4": "كم تكلفة Go؟", "go.faq.a4.p1.beforePricing": "تكلفة Go", - "go.faq.a4.p1.pricingLink": "$5 للشهر الأول", - "go.faq.a4.p1.afterPricing": "ثم $10/شهر مع حدود سخية.", + "go.faq.a4.p1.pricingLink": "$10/شهر", + "go.faq.a4.p1.afterPricing": "مع حدود سخية.", "go.faq.a4.p2.beforeAccount": "يمكنك إدارة اشتراكك في", "go.faq.a4.p2.accountLink": "حسابك", "go.faq.a4.p3": "ألغِ في أي وقت.", @@ -342,12 +341,15 @@ export const dict = { "go.faq.a5.used": "مستخدَمة", "go.faq.a5.notUsed": "غير مستخدَمة", "go.faq.a5.noAgreement": "لا توجد اتفاقية", + "go.faq.a5.notZdr": "ليست ZDR", "go.faq.a5.grokRetention": "تعطّل ZDR ميزات API مهمة تعتمد على البيانات المخزنة، بما في ذلك Responses API ذات الحالة، وFiles and Collections، وBatch API.", "go.faq.a5.gptRetention": "تُنشأ سجلات مراقبة إساءة الاستخدام لكل استخدام لميزات API، ويُحتفظ بها لمدة تصل إلى 30 يومًا.", + "go.faq.a5.museRetention": + "أسعار توكنات مخفّضة للغاية مقابل منح الإذن باستخدام مطالباتك وإكمالات النموذج لتدريب نماذج Meta المستقبلية.", "go.faq.a5.learnMore": "اعرف المزيد", - "go.faq.a5.deepseekRetention": "تُجدَّد اتفاقية ZDR شهريًا. الاتفاقية الحالية سارية حتى 31 أغسطس 2026.", + "go.faq.a5.deepseekRetention": "تُجدَّد اتفاقية ZDR شهريًا. الاتفاقية الحالية سارية حتى 30 سبتمبر 2026.", "go.faq.a5.beforeExceptions": "تتم استضافة نماذج Go في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع", "go.faq.a5.exceptionsLink": "الاستثناءات التالية", @@ -360,7 +362,7 @@ export const dict = { "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.a9": - "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة قدرها 200 طلب/يوم. يقدّم Go مجموعة منسقة من النماذج مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، وأسبوعية، وشهرية)، تعادل تقريبًا $12 لكل 5 ساعات، و$30 في الأسبوع، و$60 في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", + "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة قدرها 200 طلب/يوم. يقدّم Go مجموعة منسقة من النماذج مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، وأسبوعية، وشهرية)، تعادل الحصص الأساسية فيها تقريبًا $12 لكل 5 ساعات و$30 في الأسبوع و$60 في الشهر؛ وقد تختلف الحصص حسب النموذج (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", "zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.", "zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم", @@ -386,6 +388,9 @@ export const dict = { "zen.api.error.modelDisabled": "النموذج معطل", "zen.api.error.regionNotAllowed": "لا يتوفر أحدث إصدار من هذا النموذج إلا مستضافًا في الصين، ويتطلب تفعيلًا صريحًا: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "هذا النموذج غير متاح في بلدك.", + "zen.api.error.trainingNotAllowed": + "يجمع هذا النموذج بيانات تُستخدم لتحسين جودته، ويتطلب موافقتك الصريحة: {{consoleGoUrl}}", "zen.api.error.trialEnded": "انتهى العرض المجاني لـ {{model}}. يمكنك مواصلة استخدام النموذج بالاشتراك في OpenCode Go - {{link}}", @@ -656,23 +661,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "بضع ثوان", "workspace.lite.subscription.message": "أنت مشترك في OpenCode Go.", "workspace.lite.subscription.manage": "إدارة الاشتراك", - "workspace.lite.subscription.rollingUsage": "الاستخدام المتجدد", + "workspace.lite.subscription.rollingUsage": "الاستخدام خلال 5 ساعات", + "workspace.lite.subscription.rollingQuota": "الحصة خلال 5 ساعات", "workspace.lite.subscription.weeklyUsage": "الاستخدام الأسبوعي", + "workspace.lite.subscription.weeklyQuota": "الحصة الأسبوعية", "workspace.lite.subscription.monthlyUsage": "الاستخدام الشهري", + "workspace.lite.subscription.monthlyQuota": "الحصة الشهرية", "workspace.lite.subscription.resetsIn": "إعادة تعيين في", + "workspace.lite.subscription.showDetails": "إظهار التفاصيل", + "workspace.lite.subscription.hideDetails": "إخفاء التفاصيل", + "workspace.lite.subscription.model": "النموذج", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "الإجمالي", "workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام", "workspace.lite.subscription.selectProvider": 'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.', "workspace.lite.providers.title": "المزودون", "workspace.lite.providers.description": "تحكم في المزودين المستخدمين للتوجيه.", + "workspace.lite.providers.allowTraining": "السماح بالنماذج التي تتدرب على بيانات الطلبات", "workspace.lite.providers.useChina": "تفعيل النماذج المستضافة في الصين", "workspace.lite.black.message": "أنت مشترك حاليًا في OpenCode Black أو في قائمة الانتظار. يرجى إلغاء الاشتراك أولاً إذا كنت ترغب في التبديل إلى Go.", "workspace.lite.other.message": "عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Go. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.", "workspace.lite.promo.description": - "يبدأ OpenCode Go بسعر {{price}}، ثم $10/شهر، ويوفر وصولا موثوقا لنماذج البرمجة المفتوحة الشهيرة مع حدود استخدام سخية.", - "workspace.lite.promo.price": "$5 للشهر الأول", + "يبلغ سعر OpenCode Go {{price}}، ويوفر وصولا موثوقا لنماذج البرمجة المفتوحة الشهيرة مع حدود استخدام سخية.", + "workspace.lite.promo.price": "$10/شهر", "workspace.lite.promo.modelsTitle": "ما يتضمنه", "workspace.lite.promo.footer": "صُممت الخطة بشكل أساسي للمستخدمين الدوليين، وتوفر وصولًا عالميًا مستقرًا. قد تتغير الأسعار وحدود الاستخدام بينما نتعلم من الاستخدام المبكر والملاحظات.", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index f2ca27f93c..7faba59026 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -256,9 +256,9 @@ export const dict = { "zen.privacy.exceptionsLink": "seguintes exceções", "go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos", - "go.banner.text": "DeepSeek V4 Flash tem limites de uso 2x maiores por tempo limitado", + "go.banner.text": "GLM-5.3-Flash tem limites de uso 2x maiores por tempo limitado", "go.meta.description": - "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de uso e acesso confiável aos principais modelos de codificação.", + "O Go custa $10/mês, com limites generosos de uso e acesso confiável aos principais modelos de codificação.", "go.hero.title": "Modelos de codificação de baixo custo para todos", "go.hero.body": "O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.", @@ -267,13 +267,14 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Assinar o Go", "go.cta.price": "$10/mês", - "go.cta.promo": "$5 no primeiro mês", "go.pricing.body": - "Use com qualquer agente. $5 no primeiro mês, depois $10/mês. Recarregue o crédito se necessário. Cancele a qualquer momento.", + "Use com qualquer agente. $10/mês. Recarregue o crédito se necessário. Cancele a qualquer momento.", "go.graph.free": "Grátis", "go.graph.freePill": "Big Pickle e modelos gratuitos", "go.graph.go": "Go", "go.graph.label": "Requisições por 5 horas", + "go.graph.limitedRegions": "regiões limitadas", + "go.graph.limitedTime": "tempo limitado", "go.graph.usageLimits": "Limites de uso", "go.graph.aria": "Requisições por 5h: {{free}} vs {{go}}", @@ -301,21 +302,20 @@ export const dict = { "go.testimonials.frank.quote": "Eu queria ainda estar na Nvidia.", "go.problem.title": "Que problema o Go resolve?", "go.problem.body": - "Estamos focados em levar a experiência do OpenCode para o maior número de pessoas possível. OpenCode Go é uma assinatura de baixo custo: $5 no primeiro mês, depois $10/mês. Oferece limites generosos e acesso confiável aos modelos open source mais capazes.", + "Estamos focados em levar a experiência do OpenCode para o maior número de pessoas possível. OpenCode Go é uma assinatura de baixo custo de $10/mês. Oferece limites generosos e acesso confiável aos modelos open source mais capazes.", "go.problem.subtitle": " ", "go.problem.item1": "Preço de assinatura de baixo custo", "go.problem.item2": "Limites generosos e acesso confiável", "go.problem.item3": "Feito para o maior número possível de programadores", "go.problem.item4": "Uma seleção de modelos testados para codificação com agentes", "go.how.title": "Como o Go funciona", - "go.how.body": - "O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.", + "go.how.body": "O Go custa $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.", "go.how.step1.title": "Crie uma conta", "go.how.step1.beforeLink": "siga as", "go.how.step1.link": "instruções de configuração", "go.how.step2.title": "Assinar o Go", - "go.how.step2.link": "$5 no primeiro mês", - "go.how.step2.afterLink": "depois $10/mês com limites generosos", + "go.how.step2.link": "$10/mês", + "go.how.step2.afterLink": "com limites generosos", "go.how.step3.title": "Comece a codificar", "go.how.step3.body": "com acesso confiável a modelos de código aberto", "go.privacy.title": "Sua privacidade é importante para nós", @@ -332,11 +332,11 @@ export const dict = { "go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.", "go.faq.q3": "O Go é o mesmo que o Zen?", "go.faq.a3": - "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável a uma seleção de modelos.", + "Não. Zen é pay-as-you-go, enquanto o Go custa $10/mês, com limites generosos e acesso confiável a uma seleção de modelos.", "go.faq.q4": "Quanto custa o Go?", "go.faq.a4.p1.beforePricing": "O Go custa", - "go.faq.a4.p1.pricingLink": "$5 no primeiro mês", - "go.faq.a4.p1.afterPricing": "depois $10/mês com limites generosos.", + "go.faq.a4.p1.pricingLink": "$10/mês", + "go.faq.a4.p1.afterPricing": "com limites generosos.", "go.faq.a4.p2.beforeAccount": "Você pode gerenciar sua assinatura em sua", "go.faq.a4.p2.accountLink": "conta", "go.faq.a4.p3": "Cancele a qualquer momento.", @@ -349,13 +349,16 @@ export const dict = { "go.faq.a5.used": "Usado", "go.faq.a5.notUsed": "Não usado", "go.faq.a5.noAgreement": "Sem acordo", + "go.faq.a5.notZdr": "Não é ZDR", "go.faq.a5.grokRetention": "O ZDR desativa recursos importantes da API que dependem de dados armazenados, incluindo a Responses API com estado, Files and Collections e a Batch API.", "go.faq.a5.gptRetention": "Logs de monitoramento de abuso são gerados para todo uso de recursos da API e retidos por até 30 dias.", + "go.faq.a5.museRetention": + "Preços de tokens com grandes descontos em troca da permissão para usar seus prompts e respostas geradas para treinar futuros modelos da Meta.", "go.faq.a5.learnMore": "Saiba mais", "go.faq.a5.deepseekRetention": - "O acordo de ZDR é renovado mensalmente. O acordo atual é válido até 31 de agosto de 2026.", + "O acordo de ZDR é renovado mensalmente. O acordo atual é válido até 30 de setembro de 2026.", "go.faq.a5.beforeExceptions": "Os modelos Go são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos, com as", "go.faq.a5.exceptionsLink": "seguintes exceções", @@ -369,7 +372,7 @@ export const dict = { "go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?", "go.faq.a9": - "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go oferece uma seleção de modelos com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", + "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go oferece uma seleção de modelos com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a cotas básicas de $12 por 5 horas, $30 por semana e $60 por mês; as cotas específicas podem variar por modelo (as contagens reais de requisições variam de acordo com o modelo e o uso).", "zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} não suportado", @@ -395,6 +398,9 @@ export const dict = { "zen.api.error.modelDisabled": "O modelo está desabilitado", "zen.api.error.regionNotAllowed": "A versão mais recente deste modelo está disponível apenas com hospedagem na China e requer adesão explícita: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Este modelo não está disponível no seu país.", + "zen.api.error.trainingNotAllowed": + "Este modelo coleta dados usados para melhorar sua qualidade e exige seu consentimento explícito: {{consoleGoUrl}}", "zen.api.error.trialEnded": "A promoção gratuita do {{model}} terminou. Você pode continuar usando o modelo assinando o OpenCode Go - {{link}}", @@ -667,23 +673,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "alguns segundos", "workspace.lite.subscription.message": "Você assina o OpenCode Go.", "workspace.lite.subscription.manage": "Gerenciar Assinatura", - "workspace.lite.subscription.rollingUsage": "Uso Contínuo", + "workspace.lite.subscription.rollingUsage": "Uso de 5 horas", + "workspace.lite.subscription.rollingQuota": "Cota de 5 horas", "workspace.lite.subscription.weeklyUsage": "Uso Semanal", + "workspace.lite.subscription.weeklyQuota": "Cota Semanal", "workspace.lite.subscription.monthlyUsage": "Uso Mensal", + "workspace.lite.subscription.monthlyQuota": "Cota Mensal", "workspace.lite.subscription.resetsIn": "Reinicia em", + "workspace.lite.subscription.showDetails": "Mostrar detalhes", + "workspace.lite.subscription.hideDetails": "Ocultar detalhes", + "workspace.lite.subscription.model": "Modelo", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Total", "workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso", "workspace.lite.subscription.selectProvider": 'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.', "workspace.lite.providers.title": "Provedores", "workspace.lite.providers.description": "Controle quais provedores são usados para roteamento.", + "workspace.lite.providers.allowTraining": "Permitir modelos que usam dados de solicitações para treinamento", "workspace.lite.providers.useChina": "Ativar modelos hospedados na China", "workspace.lite.black.message": "Você está atualmente inscrito no OpenCode Black ou na lista de espera. Por favor, cancele a assinatura primeiro se desejar mudar para o Go.", "workspace.lite.other.message": "Outro membro neste workspace já assina o OpenCode Go. Apenas um membro por workspace pode assinar.", "workspace.lite.promo.description": - "O OpenCode Go começa em {{price}}, depois $10/mês, e oferece acesso confiável a modelos de codificação abertos populares com limites de uso generosos.", - "workspace.lite.promo.price": "$5 no primeiro mês", + "O OpenCode Go custa {{price}} e oferece acesso confiável a modelos de codificação abertos populares com limites de uso generosos.", + "workspace.lite.promo.price": "$10/mês", "workspace.lite.promo.modelsTitle": "O que está incluído", "workspace.lite.promo.footer": "O plano foi desenvolvido principalmente para usuários internacionais e oferece acesso global estável. Os preços e limites de uso podem mudar à medida que aprendemos com o uso inicial e o feedback recebido.", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 99ac8a1799..a620395938 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -254,9 +254,9 @@ export const dict = { "zen.privacy.exceptionsLink": "følgende undtagelser", "go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle", - "go.banner.text": "DeepSeek V4 Flash får fordoblet brugsgrænse i en begrænset periode", + "go.banner.text": "GLM-5.3-Flash får fordoblet brugsgrænse i en begrænset periode", "go.meta.description": - "Go starter ved $5 for den første måned, derefter $10/måned, med generøse brugsgrænser og pålidelig adgang til førende kodningsmodeller.", + "Go koster $10/måned, med generøse brugsgrænser og pålidelig adgang til førende kodningsmodeller.", "go.hero.title": "Kodningsmodeller til lav pris for alle", "go.hero.body": "Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.", @@ -265,13 +265,13 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Abonner på Go", "go.cta.price": "$10/måned", - "go.cta.promo": "$5 første måned", - "go.pricing.body": - "Brug med enhver agent. $5 første måned, derefter $10/måned. Tank op med kredit efter behov. Afmeld når som helst.", + "go.pricing.body": "Brug med enhver agent. $10/måned. Tank op med kredit efter behov. Afmeld når som helst.", "go.graph.free": "Gratis", "go.graph.freePill": "Big Pickle og gratis modeller", "go.graph.go": "Go", "go.graph.label": "Forespørgsler pr. 5 timer", + "go.graph.limitedRegions": "begrænsede regioner", + "go.graph.limitedTime": "begrænset periode", "go.graph.usageLimits": "Brugsgrænser", "go.graph.aria": "Forespørgsler pr. 5t: {{free}} vs {{go}}", @@ -298,21 +298,20 @@ export const dict = { "go.testimonials.frank.quote": "Jeg ville ønske, jeg stadig var hos Nvidia.", "go.problem.title": "Hvilket problem løser Go?", "go.problem.body": - "Vi fokuserer på at bringe OpenCode-oplevelsen ud til så mange som muligt. OpenCode Go er et lavprisabonnement: $5 for den første måned, derefter $10/måned. Det giver generøse grænser og pålidelig adgang til de mest kapable open source-modeller.", + "Vi fokuserer på at bringe OpenCode-oplevelsen ud til så mange som muligt. OpenCode Go er et lavprisabonnement til $10/måned. Det giver generøse grænser og pålidelig adgang til de mest kapable open source-modeller.", "go.problem.subtitle": " ", "go.problem.item1": "Lavpris abonnementspriser", "go.problem.item2": "Generøse grænser og pålidelig adgang", "go.problem.item3": "Bygget til så mange programmører som muligt", "go.problem.item4": "Et kurateret modeludvalg testet til agentisk kodning", "go.how.title": "Hvordan Go virker", - "go.how.body": - "Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.", + "go.how.body": "Go koster $10/måned. Du kan bruge det med OpenCode eller enhver agent.", "go.how.step1.title": "Opret en konto", "go.how.step1.beforeLink": "følg", "go.how.step1.link": "opsætningsinstruktionerne", "go.how.step2.title": "Abonner på Go", - "go.how.step2.link": "$5 første måned", - "go.how.step2.afterLink": "derefter $10/måned med generøse grænser", + "go.how.step2.link": "$10/måned", + "go.how.step2.afterLink": "med generøse grænser", "go.how.step3.title": "Start kodning", "go.how.step3.body": "med pålidelig adgang til open source-modeller", "go.privacy.title": "Dit privatliv er vigtigt for os", @@ -329,11 +328,11 @@ export const dict = { "go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til et kurateret modeludvalg.", + "Nej. Zen er pay-as-you-go, mens Go koster $10/måned, med generøse grænser og pålidelig adgang til et kurateret modeludvalg.", "go.faq.q4": "Hvad koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", - "go.faq.a4.p1.pricingLink": "$5 første måned", - "go.faq.a4.p1.afterPricing": "derefter $10/måned med generøse grænser.", + "go.faq.a4.p1.pricingLink": "$10/måned", + "go.faq.a4.p1.afterPricing": "med generøse grænser.", "go.faq.a4.p2.beforeAccount": "Du kan administrere dit abonnement i din", "go.faq.a4.p2.accountLink": "konto", "go.faq.a4.p3": "Annuller til enhver tid.", @@ -346,13 +345,16 @@ export const dict = { "go.faq.a5.used": "Brugt", "go.faq.a5.notUsed": "Ikke brugt", "go.faq.a5.noAgreement": "Ingen aftale", + "go.faq.a5.notZdr": "Ikke ZDR", "go.faq.a5.grokRetention": "ZDR deaktiverer vigtige API-funktioner, der afhænger af lagrede data, herunder den tilstandsbevarende Responses API, Files and Collections og Batch API.", "go.faq.a5.gptRetention": "Logfiler til overvågning af misbrug genereres ved al brug af API-funktioner og opbevares i op til 30 dage.", + "go.faq.a5.museRetention": + "Kraftigt nedsatte tokenpriser til gengæld for tilladelse til at bruge dine prompts og modelsvar til at træne fremtidige Meta-modeller.", "go.faq.a5.learnMore": "Læs mere", "go.faq.a5.deepseekRetention": - "ZDR-aftalen fornyes månedligt. Den nuværende aftale er gyldig til og med 31. august 2026.", + "ZDR-aftalen fornyes månedligt. Den nuværende aftale er gyldig til og med 30. september 2026.", "go.faq.a5.beforeExceptions": "Go-modeller hostes i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning, med de", @@ -366,7 +368,7 @@ export const dict = { "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle plus kampagnemodeller, der er tilgængelige på det pågældende tidspunkt, med en kvote på 200 forespørgsler/dag. Go tilbyder et kurateret modeludvalg med højere forespørgselskvoter håndhævet over rullende perioder (5 timer, ugentligt og månedligt), omtrent svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (det faktiske antal forespørgsler varierer efter model og brug).", + "Gratis modeller inkluderer Big Pickle plus kampagnemodeller, der er tilgængelige på det pågældende tidspunkt, med en kvote på 200 forespørgsler/dag. Go tilbyder et kurateret modeludvalg med højere forespørgselskvoter håndhævet over rullende perioder (5 timer, ugentligt og månedligt), omtrent svarende til basiskvoter på $12 pr. 5 timer, $30 pr. uge og $60 pr. måned; modelspecifikke kvoter kan variere (det faktiske antal forespørgsler varierer efter model og brug).", "zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.", "zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke", @@ -392,6 +394,9 @@ export const dict = { "zen.api.error.modelDisabled": "Modellen er deaktiveret", "zen.api.error.regionNotAllowed": "Den nyeste version af denne model er kun tilgængelig som hostet i Kina og kræver, at du aktivt tilvælger den: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Denne model er ikke tilgængelig i dit land.", + "zen.api.error.trainingNotAllowed": + "Denne model indsamler data, der bruges til at forbedre dens kvalitet, og kræver, at du aktivt tilvælger den: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Den gratis kampagne for {{model}} er afsluttet. Du kan fortsætte med at bruge modellen ved at abonnere på OpenCode Go - {{link}}", @@ -664,23 +669,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "et par sekunder", "workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.", "workspace.lite.subscription.manage": "Administrer abonnement", - "workspace.lite.subscription.rollingUsage": "Løbende forbrug", + "workspace.lite.subscription.rollingUsage": "5-timers forbrug", + "workspace.lite.subscription.rollingQuota": "5-timers kvote", "workspace.lite.subscription.weeklyUsage": "Ugentligt forbrug", + "workspace.lite.subscription.weeklyQuota": "Ugentlig kvote", "workspace.lite.subscription.monthlyUsage": "Månedligt forbrug", + "workspace.lite.subscription.monthlyQuota": "Månedlig kvote", "workspace.lite.subscription.resetsIn": "Nulstiller i", + "workspace.lite.subscription.showDetails": "Vis detaljer", + "workspace.lite.subscription.hideDetails": "Skjul detaljer", + "workspace.lite.subscription.model": "Model", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "I alt", "workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne", "workspace.lite.subscription.selectProvider": 'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.', "workspace.lite.providers.title": "Udbydere", "workspace.lite.providers.description": "Styr, hvilke udbydere der bruges til routing.", + "workspace.lite.providers.allowTraining": "Tillad modeller, der træner på anmodningsdata", "workspace.lite.providers.useChina": "Aktivér modeller hostet i Kina", "workspace.lite.black.message": "Du abonnerer i øjeblikket på OpenCode Black eller er på venteliste. Afmeld venligst først, hvis du vil skifte til Go.", "workspace.lite.other.message": "Et andet medlem i dette workspace abonnerer allerede på OpenCode Go. Kun ét medlem pr. workspace kan abonnere.", "workspace.lite.promo.description": - "OpenCode Go starter ved {{price}}, derefter $10/måned, og giver pålidelig adgang til populære åbne kodningsmodeller med generøse brugsgrænser.", - "workspace.lite.promo.price": "$5 for den første måned", + "OpenCode Go koster {{price}} og giver pålidelig adgang til populære åbne kodningsmodeller med generøse brugsgrænser.", + "workspace.lite.promo.price": "$10/måned", "workspace.lite.promo.modelsTitle": "Hvad er inkluderet", "workspace.lite.promo.footer": "Planen er primært udviklet til internationale brugere og giver stabil adgang i hele verden. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af de første brugserfaringer og tilbagemeldinger.", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 86ad085e12..5dab86ea58 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -256,9 +256,9 @@ export const dict = { "zen.privacy.exceptionsLink": "folgenden Ausnahmen", "go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle", - "go.banner.text": "DeepSeek V4 Flash erhält für begrenzte Zeit 2x Nutzungslimits", + "go.banner.text": "GLM-5.3-Flash erhält für begrenzte Zeit 2x Nutzungslimits", "go.meta.description": - "Go beginnt bei $5 für deinen ersten Monat, danach $10/Monat, mit großzügigen Nutzungslimits und zuverlässigem Zugang zu führenden Coding-Modellen.", + "Go kostet $10/Monat, mit großzügigen Nutzungslimits und zuverlässigem Zugang zu führenden Coding-Modellen.", "go.hero.title": "Kostengünstige Coding-Modelle für alle", "go.hero.body": "Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.", @@ -267,13 +267,13 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Go abonnieren", "go.cta.price": "$10/Monat", - "go.cta.promo": "$5 im ersten Monat", - "go.pricing.body": - "Mit jedem Agenten nutzbar. $5 im ersten Monat, danach $10/Monat. Guthaben bei Bedarf aufladen. Jederzeit kündbar.", + "go.pricing.body": "Mit jedem Agenten nutzbar. $10/Monat. Guthaben bei Bedarf aufladen. Jederzeit kündbar.", "go.graph.free": "Kostenlos", "go.graph.freePill": "Big Pickle und kostenlose Modelle", "go.graph.go": "Go", "go.graph.label": "Anfragen pro 5 Stunden", + "go.graph.limitedRegions": "begrenzte Regionen", + "go.graph.limitedTime": "begrenzte Zeit", "go.graph.usageLimits": "Nutzungslimits", "go.graph.aria": "Anfragen pro 5h: {{free}} vs {{go}}", @@ -300,21 +300,20 @@ export const dict = { "go.testimonials.frank.quote": "Ich wünschte, ich wäre noch bei Nvidia.", "go.problem.title": "Welches Problem löst Go?", "go.problem.body": - "Wir konzentrieren uns darauf, die OpenCode-Erfahrung so vielen Menschen wie möglich zugänglich zu machen. OpenCode Go ist ein kostengünstiges Abonnement: $5 im ersten Monat, danach $10/Monat. Es bietet großzügige Limits und zuverlässigen Zugang zu den leistungsfähigsten Open-Source-Modellen.", + "Wir konzentrieren uns darauf, die OpenCode-Erfahrung so vielen Menschen wie möglich zugänglich zu machen. OpenCode Go ist ein kostengünstiges Abonnement für $10/Monat. Es bietet großzügige Limits und zuverlässigen Zugang zu den leistungsfähigsten Open-Source-Modellen.", "go.problem.subtitle": " ", "go.problem.item1": "Kostengünstiges Abonnement", "go.problem.item2": "Großzügige Limits und zuverlässiger Zugang", "go.problem.item3": "Für so viele Programmierer wie möglich gebaut", "go.problem.item4": "Eine kuratierte, für Agentic Coding getestete Modellauswahl", "go.how.title": "Wie Go funktioniert", - "go.how.body": - "Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.", + "go.how.body": "Go kostet $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.", "go.how.step1.title": "Konto erstellen", "go.how.step1.beforeLink": "folge den", "go.how.step1.link": "Einrichtungsanweisungen", "go.how.step2.title": "Go abonnieren", - "go.how.step2.link": "$5 im ersten Monat", - "go.how.step2.afterLink": "danach $10/Monat mit großzügigen Limits", + "go.how.step2.link": "$10/Monat", + "go.how.step2.afterLink": "mit großzügigen Limits", "go.how.step3.title": "Loslegen mit Coding", "go.how.step3.body": "mit zuverlässigem Zugang zu Open-Source-Modellen", "go.privacy.title": "Deine Privatsphäre ist uns wichtig", @@ -331,11 +330,11 @@ export const dict = { "go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.", "go.faq.q3": "Ist Go dasselbe wie Zen?", "go.faq.a3": - "Nein. Zen ist Pay-as-you-go, während Go bei $5 für deinen ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu einer kuratierten Modellauswahl.", + "Nein. Zen ist Pay-as-you-go, während Go $10/Monat kostet, mit großzügigen Limits und zuverlässigem Zugang zu einer kuratierten Modellauswahl.", "go.faq.q4": "Wie viel kostet Go?", "go.faq.a4.p1.beforePricing": "Go kostet", - "go.faq.a4.p1.pricingLink": "$5 im ersten Monat", - "go.faq.a4.p1.afterPricing": "danach $10/Monat mit großzügigen Limits.", + "go.faq.a4.p1.pricingLink": "$10/Monat", + "go.faq.a4.p1.afterPricing": "mit großzügigen Limits.", "go.faq.a4.p2.beforeAccount": "Du kannst dein Abonnement in deinem", "go.faq.a4.p2.accountLink": "Konto verwalten", "go.faq.a4.p3": "Jederzeit kündbar.", @@ -348,13 +347,16 @@ export const dict = { "go.faq.a5.used": "Verwendet", "go.faq.a5.notUsed": "Nicht verwendet", "go.faq.a5.noAgreement": "Keine Vereinbarung", + "go.faq.a5.notZdr": "Kein ZDR", "go.faq.a5.grokRetention": "ZDR deaktiviert wichtige API-Funktionen, die von gespeicherten Daten abhängen, einschließlich der zustandsbehafteten Responses API, Files and Collections und der Batch API.", "go.faq.a5.gptRetention": "Für die Nutzung aller API-Funktionen werden Protokolle zur Missbrauchsüberwachung erstellt und bis zu 30 Tage lang aufbewahrt.", + "go.faq.a5.museRetention": + "Stark vergünstigte Tokenpreise im Gegenzug für die Erlaubnis, deine Prompts und Vervollständigungen zum Trainieren zukünftiger Meta-Modelle zu verwenden.", "go.faq.a5.learnMore": "Mehr erfahren", "go.faq.a5.deepseekRetention": - "Die ZDR-Vereinbarung wird monatlich erneuert. Die aktuelle Vereinbarung gilt bis einschließlich 31. August 2026.", + "Die ZDR-Vereinbarung wird monatlich erneuert. Die aktuelle Vereinbarung gilt bis einschließlich 30. September 2026.", "go.faq.a5.beforeExceptions": "Go-Modelle werden in den USA gehostet. Anbieter verfolgen eine Zero-Retention-Politik und nutzen deine Daten nicht für das Training von Modellen, mit den", "go.faq.a5.exceptionsLink": "folgenden Ausnahmen", @@ -368,7 +370,7 @@ export const dict = { "go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?", "go.faq.a9": - "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go bietet eine kuratierte Modellauswahl mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", + "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go bietet eine kuratierte Modellauswahl mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu Basiskontingenten von $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat; modellspezifische Kontingente können abweichen (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", "zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.", "zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt", @@ -394,6 +396,9 @@ export const dict = { "zen.api.error.modelDisabled": "Modell ist deaktiviert", "zen.api.error.regionNotAllowed": "Die neueste Version dieses Modells ist nur verfügbar, wenn sie in China gehostet wird, und muss ausdrücklich aktiviert werden: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Dieses Modell ist in deinem Land nicht verfügbar.", + "zen.api.error.trainingNotAllowed": + "Dieses Modell erfasst Daten, die zur Verbesserung seiner Qualität verwendet werden, und erfordert deine ausdrückliche Zustimmung: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Die kostenlose Aktion für {{model}} ist beendet. Du kannst das Modell weiterhin nutzen, indem du OpenCode Go abonnierst - {{link}}", @@ -666,23 +671,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "einige Sekunden", "workspace.lite.subscription.message": "Du hast OpenCode Go abonniert.", "workspace.lite.subscription.manage": "Abo verwalten", - "workspace.lite.subscription.rollingUsage": "Fortlaufende Nutzung", + "workspace.lite.subscription.rollingUsage": "5-Stunden-Nutzung", + "workspace.lite.subscription.rollingQuota": "5-Stunden-Kontingent", "workspace.lite.subscription.weeklyUsage": "Wöchentliche Nutzung", + "workspace.lite.subscription.weeklyQuota": "Wöchentliches Kontingent", "workspace.lite.subscription.monthlyUsage": "Monatliche Nutzung", + "workspace.lite.subscription.monthlyQuota": "Monatliches Kontingent", "workspace.lite.subscription.resetsIn": "Setzt zurück in", + "workspace.lite.subscription.showDetails": "Details anzeigen", + "workspace.lite.subscription.hideDetails": "Details ausblenden", + "workspace.lite.subscription.model": "Modell", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Gesamt", "workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind", "workspace.lite.subscription.selectProvider": 'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.', "workspace.lite.providers.title": "Anbieter", "workspace.lite.providers.description": "Steuere, welche Anbieter für das Routing verwendet werden.", + "workspace.lite.providers.allowTraining": "Modelle zulassen, die mit Anfragedaten trainieren", "workspace.lite.providers.useChina": "In China gehostete Modelle aktivieren", "workspace.lite.black.message": "Du hast derzeit OpenCode Black abonniert oder stehst auf der Warteliste. Bitte kündige zuerst, wenn du zu Go wechseln möchtest.", "workspace.lite.other.message": "Ein anderes Mitglied in diesem Workspace hat OpenCode Go bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.", "workspace.lite.promo.description": - "OpenCode Go startet bei {{price}}, danach $10/Monat, und bietet zuverlässigen Zugang zu beliebten offenen Coding-Modellen mit großzügigen Nutzungslimits.", - "workspace.lite.promo.price": "$5 im ersten Monat", + "OpenCode Go kostet {{price}} und bietet zuverlässigen Zugang zu beliebten offenen Coding-Modellen mit großzügigen Nutzungslimits.", + "workspace.lite.promo.price": "$10/Monat", "workspace.lite.promo.modelsTitle": "Was enthalten ist", "workspace.lite.promo.footer": "Der Plan richtet sich in erster Linie an internationale Nutzer und bietet stabilen weltweiten Zugriff. Preise und Nutzungslimits können sich ändern, wenn wir Erkenntnisse aus der ersten Nutzung und dem Feedback gewinnen.", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index d778788beb..ee4dfe1aaf 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -253,9 +253,8 @@ export const dict = { "zen.privacy.exceptionsLink": "following exceptions", "go.title": "OpenCode Go | Low cost coding models for everyone", - "go.banner.text": "DeepSeek V4 Flash gets 2× usage limits for a limited time", - "go.meta.description": - "Go starts at $5 for your first month, then $10/month, with generous usage limits and reliable access to leading coding models.", + "go.banner.text": "GLM-5.3-Flash gets 2× usage limits for a limited time", + "go.meta.description": "Go costs $10/month, with generous usage limits and reliable access to leading coding models.", "go.hero.title": "Low cost coding models for everyone", "go.hero.body": "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.", @@ -264,12 +263,13 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Subscribe to Go", "go.cta.price": "$10/month", - "go.cta.promo": "$5 first month", - "go.pricing.body": "Use with any agent. $5 first month, then $10/month. Top up credit if needed. Cancel any time.", + "go.pricing.body": "Use with any agent. $10/month. Top up credit if needed. Cancel any time.", "go.graph.free": "Free", "go.graph.freePill": "Big Pickle and free models", "go.graph.go": "Go", - "go.graph.label": "Requests per 5 hour", + "go.graph.label": "Requests / 5 hours", + "go.graph.limitedRegions": "limited regions", + "go.graph.limitedTime": "limited time", "go.graph.tick": "{{n}}x", "go.graph.usageLimits": "Usage limits", "go.graph.aria": "Requests per 5h: {{free}} vs {{go}}", @@ -297,20 +297,20 @@ export const dict = { "go.testimonials.frank.quote": "I wish I was still at Nvidia.", "go.problem.title": "What problem is Go solving?", "go.problem.body": - "We're focused on bringing the OpenCode experience to as many people as possible. OpenCode Go is a low cost subscription: $5 for your first month, then $10/month. It provides generous limits and reliable access to the most capable open source models.", + "We're focused on bringing the OpenCode experience to as many people as possible. OpenCode Go is a low cost $10/month subscription. It provides generous limits and reliable access to the most capable open source models.", "go.problem.subtitle": " ", "go.problem.item1": "Low cost subscription pricing", "go.problem.item2": "Generous limits and reliable access", "go.problem.item3": "Built for as many programmers as possible", "go.problem.item4": "A curated model lineup tested for agentic coding", "go.how.title": "How Go works", - "go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.", + "go.how.body": "Go costs $10/month. You can use it with OpenCode or any agent.", "go.how.step1.title": "Create an account", "go.how.step1.beforeLink": "follow the", "go.how.step1.link": "setup instructions", "go.how.step2.title": "Subscribe to Go", - "go.how.step2.link": "$5 first month", - "go.how.step2.afterLink": "then $10/month with generous limits", + "go.how.step2.link": "$10/month", + "go.how.step2.afterLink": "with generous limits", "go.how.step3.title": "Start coding", "go.how.step3.body": "with reliable access to open-source models", "go.privacy.title": "Your privacy is important to us", @@ -327,11 +327,11 @@ export const dict = { "go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.", "go.faq.q3": "Is Go the same as Zen?", "go.faq.a3": - "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to a curated model lineup.", + "No. Zen is pay-as-you-go, while Go costs $10/month, with generous limits and reliable access to a curated model lineup.", "go.faq.q4": "How much does Go cost?", "go.faq.a4.p1.beforePricing": "Go costs", - "go.faq.a4.p1.pricingLink": "$5 first month", - "go.faq.a4.p1.afterPricing": "then $10/month with generous limits.", + "go.faq.a4.p1.pricingLink": "$10/month", + "go.faq.a4.p1.afterPricing": "with generous limits.", "go.faq.a4.p2.beforeAccount": "You can manage your subscription in your", "go.faq.a4.p2.accountLink": "account", "go.faq.a4.p3": "Cancel any time.", @@ -344,12 +344,15 @@ export const dict = { "go.faq.a5.used": "Used", "go.faq.a5.notUsed": "Not used", "go.faq.a5.noAgreement": "No agreement", + "go.faq.a5.notZdr": "Not ZDR", "go.faq.a5.grokRetention": "ZDR disables important API features that depend on stored data, including the stateful Responses API, Files and Collections, and the Batch API.", "go.faq.a5.gptRetention": "Abuse monitoring logs are generated for all API feature usage and retained for up to 30 days.", + "go.faq.a5.museRetention": + "Heavily discounted token pricing in exchange for permission to use your prompts and completions to train future Meta models.", "go.faq.a5.deepseekRetention": - "ZDR agreement is renewed monthly. The current agreement is valid through August 31, 2026.", + "ZDR agreement is renewed monthly. The current agreement is valid through September 30, 2026.", "go.faq.a5.learnMore": "Learn more", "go.faq.a5.beforeExceptions": @@ -364,7 +367,7 @@ export const dict = { "go.faq.q9": "What is the difference between free models and Go?", "go.faq.a9": - "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go offers a curated model lineup with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", + "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go offers a curated model lineup with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to base allowances of $12 per 5 hours, $30 per week, and $60 per month; model-specific allowances may differ (actual request counts vary by model and usage).", "zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.", "zen.api.error.modelNotSupported": "Model {{model}} is not supported", @@ -391,6 +394,9 @@ export const dict = { "zen.api.error.modelDisabled": "Model is disabled", "zen.api.error.regionNotAllowed": "The latest version of this model is only available hosted in China and requires explicit opt in: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "This model is not available in your country.", + "zen.api.error.trainingNotAllowed": + "This model collects data used to improve its quality and requires explicit opt in: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}", @@ -663,23 +669,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "a few seconds", "workspace.lite.subscription.message": "You are subscribed to OpenCode Go.", "workspace.lite.subscription.manage": "Manage Subscription", - "workspace.lite.subscription.rollingUsage": "Rolling Usage", + "workspace.lite.subscription.rollingUsage": "5-hour Usage", + "workspace.lite.subscription.rollingQuota": "5-hour Quota", "workspace.lite.subscription.weeklyUsage": "Weekly Usage", + "workspace.lite.subscription.weeklyQuota": "Weekly Quota", "workspace.lite.subscription.monthlyUsage": "Monthly Usage", + "workspace.lite.subscription.monthlyQuota": "Monthly Quota", "workspace.lite.subscription.resetsIn": "Resets in", + "workspace.lite.subscription.showDetails": "Show details", + "workspace.lite.subscription.hideDetails": "Hide details", + "workspace.lite.subscription.model": "Model", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Total", "workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits", "workspace.lite.subscription.selectProvider": 'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.', "workspace.lite.providers.title": "Providers", "workspace.lite.providers.description": "Control which providers are used for routing.", - "workspace.lite.providers.useChina": "Enable models hosted in China", + "workspace.lite.providers.allowTraining": "Allow models that train on request data", + "workspace.lite.providers.useChina": "Allow models hosted in China", "workspace.lite.black.message": "You're currently subscribed to OpenCode Black or on the waitlist. Please unsubscribe first if you'd like to switch to Go.", "workspace.lite.other.message": "Another member in this workspace is already subscribed to OpenCode Go. Only one member per workspace can subscribe.", "workspace.lite.promo.description": - "OpenCode Go starts at {{price}}, then $10/month, and provides reliable access to popular open coding models with generous usage limits.", - "workspace.lite.promo.price": "$5 for your first month", + "OpenCode Go costs {{price}} and provides reliable access to popular open coding models with generous usage limits.", + "workspace.lite.promo.price": "$10/month", "workspace.lite.promo.modelsTitle": "What's Included", "workspace.lite.promo.footer": "The plan is designed primarily for international users and provides stable global access. Pricing and usage limits may change as we learn from early usage and feedback.", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index 24c28f3c01..f9c7bbbd52 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -257,9 +257,9 @@ export const dict = { "zen.privacy.exceptionsLink": "siguientes excepciones", "go.title": "OpenCode Go | Modelos de programación de bajo coste para todos", - "go.banner.text": "DeepSeek V4 Flash tiene límites de uso 2x mayores por tiempo limitado", + "go.banner.text": "GLM-5.3-Flash tiene límites de uso 2x mayores por tiempo limitado", "go.meta.description": - "Go comienza en $5 el primer mes, luego 10 $/mes, con límites de uso generosos y acceso fiable a modelos de programación líderes.", + "Go cuesta 10 $/mes, con límites de uso generosos y acceso fiable a modelos de programación líderes.", "go.hero.title": "Modelos de programación de bajo coste para todos", "go.hero.body": "Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.", @@ -268,13 +268,14 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Suscribirse a Go", "go.cta.price": "10 $/mes", - "go.cta.promo": "$5 el primer mes", "go.pricing.body": - "Úsalo con cualquier agente. $5 el primer mes, luego 10 $/mes. Recarga crédito si es necesario. Cancela en cualquier momento.", + "Úsalo con cualquier agente. 10 $/mes. Recarga crédito si es necesario. Cancela en cualquier momento.", "go.graph.free": "Gratis", "go.graph.freePill": "Big Pickle y modelos gratuitos", "go.graph.go": "Go", "go.graph.label": "Solicitudes por 5 horas", + "go.graph.limitedRegions": "regiones limitadas", + "go.graph.limitedTime": "tiempo limitado", "go.graph.usageLimits": "Límites de uso", "go.graph.aria": "Solicitudes por 5h: {{free}} vs {{go}}", @@ -302,20 +303,20 @@ export const dict = { "go.testimonials.frank.quote": "Ojalá siguiera en Nvidia.", "go.problem.title": "¿Qué problema resuelve Go?", "go.problem.body": - "Nos enfocamos en llevar la experiencia de OpenCode a tantas personas como sea posible. OpenCode Go es una suscripción de bajo coste: $5 el primer mes, luego 10 $/mes. Proporciona límites generosos y acceso fiable a los modelos de código abierto más capaces.", + "Nos enfocamos en llevar la experiencia de OpenCode a tantas personas como sea posible. OpenCode Go es una suscripción de bajo coste de 10 $/mes. Proporciona límites generosos y acceso fiable a los modelos de código abierto más capaces.", "go.problem.subtitle": " ", "go.problem.item1": "Precios de suscripción de bajo coste", "go.problem.item2": "Límites generosos y acceso fiable", "go.problem.item3": "Creado para tantos programadores como sea posible", "go.problem.item4": "Una selección de modelos probados para programación agéntica", "go.how.title": "Cómo funciona Go", - "go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.", + "go.how.body": "Go cuesta 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.", "go.how.step1.title": "Crear una cuenta", "go.how.step1.beforeLink": "sigue las", "go.how.step1.link": "instrucciones de configuración", "go.how.step2.title": "Suscribirse a Go", - "go.how.step2.link": "$5 el primer mes", - "go.how.step2.afterLink": "luego 10 $/mes con límites generosos", + "go.how.step2.link": "10 $/mes", + "go.how.step2.afterLink": "con límites generosos", "go.how.step3.title": "Empezar a programar", "go.how.step3.body": "con acceso fiable a modelos de código abierto", "go.privacy.title": "Tu privacidad es importante para nosotros", @@ -332,11 +333,11 @@ export const dict = { "go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.", "go.faq.q3": "¿Es Go lo mismo que Zen?", "go.faq.a3": - "No. Zen es de pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a una selección de modelos.", + "No. Zen es de pago por uso, mientras que Go cuesta 10 $/mes, con límites generosos y acceso fiable a una selección de modelos.", "go.faq.q4": "¿Cuánto cuesta Go?", "go.faq.a4.p1.beforePricing": "Go cuesta", - "go.faq.a4.p1.pricingLink": "$5 el primer mes", - "go.faq.a4.p1.afterPricing": "luego 10 $/mes con límites generosos.", + "go.faq.a4.p1.pricingLink": "10 $/mes", + "go.faq.a4.p1.afterPricing": "con límites generosos.", "go.faq.a4.p2.beforeAccount": "Puedes gestionar tu suscripción en tu", "go.faq.a4.p2.accountLink": "cuenta", "go.faq.a4.p3": "Cancela en cualquier momento.", @@ -349,13 +350,16 @@ export const dict = { "go.faq.a5.used": "Utilizado", "go.faq.a5.notUsed": "No utilizado", "go.faq.a5.noAgreement": "Sin acuerdo", + "go.faq.a5.notZdr": "Sin ZDR", "go.faq.a5.grokRetention": "ZDR deshabilita funciones importantes de la API que dependen de datos almacenados, incluidas la Responses API con estado, Files and Collections y la Batch API.", "go.faq.a5.gptRetention": "Se generan registros de supervisión de abusos para todo el uso de funciones de la API y se conservan durante un máximo de 30 días.", + "go.faq.a5.museRetention": + "Precios de tokens muy reducidos a cambio de permitir que tus prompts y las respuestas generadas se utilicen para entrenar futuros modelos de Meta.", "go.faq.a5.learnMore": "Más información", "go.faq.a5.deepseekRetention": - "El acuerdo de ZDR se renueva mensualmente. El acuerdo actual es válido hasta el 31 de agosto de 2026.", + "El acuerdo de ZDR se renueva mensualmente. El acuerdo actual es válido hasta el 30 de septiembre de 2026.", "go.faq.a5.beforeExceptions": "Los modelos de Go están alojados en EE. UU. Los proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos, con las", "go.faq.a5.exceptionsLink": "siguientes excepciones", @@ -369,7 +373,7 @@ export const dict = { "go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?", "go.faq.a9": - "Los modelos gratuitos incluyen Big Pickle y los modelos promocionales disponibles en ese momento, con una cuota de 200 solicitudes/día. Go ofrece una selección de modelos con cuotas de solicitudes más altas aplicadas en ventanas móviles (de 5 horas, semanales y mensuales), aproximadamente equivalentes a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (la cantidad real de solicitudes varía según el modelo y el uso).", + "Los modelos gratuitos incluyen Big Pickle y los modelos promocionales disponibles en ese momento, con una cuota de 200 solicitudes/día. Go ofrece una selección de modelos con cuotas de solicitudes más altas aplicadas en ventanas móviles (de 5 horas, semanales y mensuales), aproximadamente equivalentes a cuotas base de 12 $ por 5 horas, 30 $ por semana y 60 $ por mes; las cuotas específicas pueden variar según el modelo (la cantidad real de solicitudes varía según el modelo y el uso).", "zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} no soportado", @@ -395,6 +399,9 @@ export const dict = { "zen.api.error.modelDisabled": "El modelo está deshabilitado", "zen.api.error.regionNotAllowed": "La versión más reciente de este modelo solo está disponible alojada en China y requiere una aceptación explícita: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Este modelo no está disponible en tu país.", + "zen.api.error.trainingNotAllowed": + "Este modelo recopila datos que se utilizan para mejorar su calidad y requiere tu consentimiento explícito: {{consoleGoUrl}}", "zen.api.error.trialEnded": "La promoción gratuita de {{model}} ha finalizado. Puedes seguir usando el modelo suscribiéndote a OpenCode Go - {{link}}", @@ -667,23 +674,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "unos pocos segundos", "workspace.lite.subscription.message": "Estás suscrito a OpenCode Go.", "workspace.lite.subscription.manage": "Gestionar Suscripción", - "workspace.lite.subscription.rollingUsage": "Uso Continuo", + "workspace.lite.subscription.rollingUsage": "Uso de 5 horas", + "workspace.lite.subscription.rollingQuota": "Cuota de 5 horas", "workspace.lite.subscription.weeklyUsage": "Uso Semanal", + "workspace.lite.subscription.weeklyQuota": "Cuota Semanal", "workspace.lite.subscription.monthlyUsage": "Uso Mensual", + "workspace.lite.subscription.monthlyQuota": "Cuota Mensual", "workspace.lite.subscription.resetsIn": "Se reinicia en", + "workspace.lite.subscription.showDetails": "Mostrar detalles", + "workspace.lite.subscription.hideDetails": "Ocultar detalles", + "workspace.lite.subscription.model": "Modelo", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Total", "workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso", "workspace.lite.subscription.selectProvider": 'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.', "workspace.lite.providers.title": "Proveedores", "workspace.lite.providers.description": "Controla qué proveedores se usan para el enrutamiento.", + "workspace.lite.providers.allowTraining": "Permitir modelos que se entrenan con los datos de las solicitudes", "workspace.lite.providers.useChina": "Activar modelos alojados en China", "workspace.lite.black.message": "Actualmente estás suscrito a OpenCode Black o estás en la lista de espera. Por favor, cancela la suscripción primero si deseas cambiar a Go.", "workspace.lite.other.message": "Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Go. Solo un miembro por espacio de trabajo puede suscribirse.", "workspace.lite.promo.description": - "OpenCode Go comienza en {{price}}, luego $10/mes, y ofrece acceso confiable a modelos de codificación abiertos populares con límites de uso generosos.", - "workspace.lite.promo.price": "$5 el primer mes", + "OpenCode Go cuesta {{price}} y ofrece acceso confiable a modelos de codificación abiertos populares con límites de uso generosos.", + "workspace.lite.promo.price": "$10/mes", "workspace.lite.promo.modelsTitle": "Qué incluye", "workspace.lite.promo.footer": "El plan está diseñado principalmente para usuarios internacionales y ofrece un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y de los comentarios recibidos.", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 9ecb2f65ce..ea7fccc63d 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -258,9 +258,9 @@ export const dict = { "zen.privacy.exceptionsLink": "exceptions suivantes", "go.title": "OpenCode Go | Modèles de code à faible coût pour tous", - "go.banner.text": "DeepSeek V4 Flash bénéficie de limites d’utilisation 2x supérieures pour une durée limitée", + "go.banner.text": "GLM-5.3-Flash bénéficie de limites d’utilisation 2x supérieures pour une durée limitée", "go.meta.description": - "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites d'utilisation généreuses et un accès fiable aux principaux modèles de codage.", + "Go coûte 10 $/mois, avec des limites d'utilisation généreuses et un accès fiable aux principaux modèles de codage.", "go.hero.title": "Modèles de code à faible coût pour tous", "go.hero.body": "Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.", @@ -269,13 +269,14 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "S'abonner à Go", "go.cta.price": "10 $/mois", - "go.cta.promo": "$5 le premier mois", "go.pricing.body": - "Utilisez-le avec n'importe quel agent. $5 le premier mois, puis 10 $/mois. Rechargez du crédit si nécessaire. Annulez à tout moment.", + "Utilisez-le avec n'importe quel agent. 10 $/mois. Rechargez du crédit si nécessaire. Annulez à tout moment.", "go.graph.free": "Gratuit", "go.graph.freePill": "Big Pickle et modèles gratuits", "go.graph.go": "Go", "go.graph.label": "Requêtes par tranche de 5 heures", + "go.graph.limitedRegions": "régions limitées", + "go.graph.limitedTime": "durée limitée", "go.graph.usageLimits": "Limites d'utilisation", "go.graph.aria": "Requêtes par 5h : {{free}} vs {{go}}", @@ -302,21 +303,20 @@ export const dict = { "go.testimonials.frank.quote": "J'aimerais être encore chez Nvidia.", "go.problem.title": "Quel problème Go résout-il ?", "go.problem.body": - "Nous nous efforçons d'apporter l'expérience OpenCode au plus grand nombre. OpenCode Go est un abonnement à faible coût : $5 pour le premier mois, puis 10 $/mois. Il offre des limites généreuses et un accès fiable aux modèles open source les plus performants.", + "Nous nous efforçons d'apporter l'expérience OpenCode au plus grand nombre. OpenCode Go est un abonnement à faible coût de 10 $/mois. Il offre des limites généreuses et un accès fiable aux modèles open source les plus performants.", "go.problem.subtitle": " ", "go.problem.item1": "Prix d'abonnement bas", "go.problem.item2": "Limites généreuses et accès fiable", "go.problem.item3": "Conçu pour autant de programmeurs que possible", "go.problem.item4": "Une sélection de modèles testés pour le codage agentique", "go.how.title": "Comment fonctionne Go", - "go.how.body": - "Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.", + "go.how.body": "Go coûte 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.", "go.how.step1.title": "Créez un compte", "go.how.step1.beforeLink": "suivez les", "go.how.step1.link": "instructions de configuration", "go.how.step2.title": "Abonnez-vous à Go", - "go.how.step2.link": "$5 le premier mois", - "go.how.step2.afterLink": "puis 10 $/mois avec des limites généreuses", + "go.how.step2.link": "10 $/mois", + "go.how.step2.afterLink": "avec des limites généreuses", "go.how.step3.title": "Commencez à coder", "go.how.step3.body": "avec un accès fiable aux modèles open source", "go.privacy.title": "Votre vie privée est importante pour nous", @@ -333,11 +333,11 @@ export const dict = { "go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.", "go.faq.q3": "Est-ce que Go est la même chose que Zen ?", "go.faq.a3": - "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable à une sélection de modèles.", + "Non. Zen est un paiement à l'utilisation, tandis que Go coûte 10 $/mois, avec des limites généreuses et un accès fiable à une sélection de modèles.", "go.faq.q4": "Combien coûte Go ?", "go.faq.a4.p1.beforePricing": "Go coûte", - "go.faq.a4.p1.pricingLink": "$5 le premier mois", - "go.faq.a4.p1.afterPricing": "puis 10 $/mois avec des limites généreuses.", + "go.faq.a4.p1.pricingLink": "10 $/mois", + "go.faq.a4.p1.afterPricing": "avec des limites généreuses.", "go.faq.a4.p2.beforeAccount": "Vous pouvez gérer votre abonnement dans votre", "go.faq.a4.p2.accountLink": "compte", "go.faq.a4.p3": "Annulez à tout moment.", @@ -350,13 +350,16 @@ export const dict = { "go.faq.a5.used": "Utilisé", "go.faq.a5.notUsed": "Non utilisé", "go.faq.a5.noAgreement": "Aucun accord", + "go.faq.a5.notZdr": "Pas de ZDR", "go.faq.a5.grokRetention": "Le ZDR désactive d'importantes fonctionnalités API qui dépendent des données stockées, notamment Responses API avec état, Files and Collections et Batch API.", "go.faq.a5.gptRetention": "Des journaux de surveillance des abus sont générés pour toute utilisation des fonctionnalités API et conservés pendant un maximum de 30 jours.", + "go.faq.a5.museRetention": + "Des tarifs de tokens fortement réduits en échange de l’autorisation d’utiliser vos prompts et vos complétions pour entraîner de futurs modèles Meta.", "go.faq.a5.learnMore": "En savoir plus", "go.faq.a5.deepseekRetention": - "L’accord ZDR est renouvelé chaque mois. L’accord actuel est valable jusqu’au 31 août 2026.", + "L’accord ZDR est renouvelé chaque mois. L’accord actuel est valable jusqu’au 30 septembre 2026.", "go.faq.a5.beforeExceptions": "Les modèles Go sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les", @@ -370,7 +373,7 @@ export const dict = { "Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.", "go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?", "go.faq.a9": - "Les modèles gratuits incluent Big Pickle ainsi que les modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go propose une sélection de modèles avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalents à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", + "Les modèles gratuits incluent Big Pickle ainsi que les modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go propose une sélection de modèles avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalents à des quotas de base de 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois ; les quotas propres à chaque modèle peuvent varier (le nombre réel de requêtes varie selon le modèle et l'utilisation).", "zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.", "zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge", @@ -396,6 +399,9 @@ export const dict = { "zen.api.error.modelDisabled": "Le modèle est désactivé", "zen.api.error.regionNotAllowed": "La dernière version de ce modèle est uniquement disponible avec un hébergement en Chine et nécessite votre consentement explicite : {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Ce modèle n’est pas disponible dans votre pays.", + "zen.api.error.trainingNotAllowed": + "Ce modèle collecte des données utilisées pour améliorer sa qualité et nécessite votre consentement explicite : {{consoleGoUrl}}", "zen.api.error.trialEnded": "La promotion gratuite de {{model}} est terminée. Vous pouvez continuer à utiliser le modèle en vous abonnant à OpenCode Go - {{link}}", @@ -673,24 +679,33 @@ export const dict = { "workspace.lite.time.fewSeconds": "quelques secondes", "workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Go.", "workspace.lite.subscription.manage": "Gérer l'abonnement", - "workspace.lite.subscription.rollingUsage": "Utilisation glissante", + "workspace.lite.subscription.rollingUsage": "Utilisation sur 5 heures", + "workspace.lite.subscription.rollingQuota": "Quota sur 5 heures", "workspace.lite.subscription.weeklyUsage": "Utilisation hebdomadaire", + "workspace.lite.subscription.weeklyQuota": "Quota hebdomadaire", "workspace.lite.subscription.monthlyUsage": "Utilisation mensuelle", + "workspace.lite.subscription.monthlyQuota": "Quota mensuel", "workspace.lite.subscription.resetsIn": "Réinitialisation dans", + "workspace.lite.subscription.showDetails": "Afficher les détails", + "workspace.lite.subscription.hideDetails": "Masquer les détails", + "workspace.lite.subscription.model": "Modèle", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Total", "workspace.lite.subscription.useBalance": "Utilisez votre solde disponible après avoir atteint les limites d'utilisation", "workspace.lite.subscription.selectProvider": 'Sélectionnez "OpenCode Go" comme fournisseur dans votre configuration opencode pour utiliser les modèles Go.', "workspace.lite.providers.title": "Fournisseurs", "workspace.lite.providers.description": "Contrôlez les fournisseurs utilisés pour le routage.", + "workspace.lite.providers.allowTraining": "Autoriser les modèles qui s’entraînent sur les données des requêtes", "workspace.lite.providers.useChina": "Activer les modèles hébergés en Chine", "workspace.lite.black.message": "Vous êtes actuellement abonné à OpenCode Black ou sur liste d'attente. Veuillez d'abord vous désabonner si vous souhaitez passer à Go.", "workspace.lite.other.message": "Un autre membre de cet espace de travail est déjà abonné à OpenCode Go. Un seul membre par espace de travail peut s'abonner.", "workspace.lite.promo.description": - "OpenCode Go commence à {{price}}, puis 10 $/mois, et offre un accès fiable aux modèles de code ouverts populaires avec des limites d'utilisation généreuses.", - "workspace.lite.promo.price": "$5 le premier mois", + "OpenCode Go coûte {{price}} et offre un accès fiable aux modèles de code ouverts populaires avec des limites d'utilisation généreuses.", + "workspace.lite.promo.price": "10 $/mois", "workspace.lite.promo.modelsTitle": "Ce qui est inclus", "workspace.lite.promo.footer": "Ce forfait est principalement conçu pour les utilisateurs internationaux et offre un accès mondial stable. Les tarifs et les limites d'utilisation peuvent évoluer à mesure que nous tirons les enseignements des premières utilisations et des retours reçus.", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 5203cf82df..fe44d02e02 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -254,9 +254,9 @@ export const dict = { "zen.privacy.exceptionsLink": "seguenti eccezioni", "go.title": "OpenCode Go | Modelli di coding a basso costo per tutti", - "go.banner.text": "DeepSeek V4 Flash offre limiti di utilizzo 2x superiori per un periodo limitato", + "go.banner.text": "GLM-5.3-Flash offre limiti di utilizzo 2x superiori per un periodo limitato", "go.meta.description": - "Go inizia a $5 per il primo mese, poi $10/mese, con limiti di utilizzo generosi e un accesso affidabile ai principali modelli di coding.", + "Go costa $10/mese, con limiti di utilizzo generosi e un accesso affidabile ai principali modelli di coding.", "go.hero.title": "Modelli di coding a basso costo per tutti", "go.hero.body": "Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.", @@ -265,13 +265,14 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Abbonati a Go", "go.cta.price": "$10/mese", - "go.cta.promo": "$5 il primo mese", "go.pricing.body": - "Usalo con qualsiasi agente. $5 il primo mese, poi $10/mese. Ricarica il credito se necessario. Annulla in qualsiasi momento.", + "Usalo con qualsiasi agente. $10/mese. Ricarica il credito se necessario. Annulla in qualsiasi momento.", "go.graph.free": "Gratis", "go.graph.freePill": "Big Pickle e modelli gratuiti", "go.graph.go": "Go", "go.graph.label": "Richieste ogni 5 ore", + "go.graph.limitedRegions": "regioni limitate", + "go.graph.limitedTime": "periodo limitato", "go.graph.usageLimits": "Limiti di utilizzo", "go.graph.aria": "Richieste ogni 5h: {{free}} vs {{go}}", @@ -298,20 +299,20 @@ export const dict = { "go.testimonials.frank.quote": "Vorrei essere ancora a Nvidia.", "go.problem.title": "Quale problema risolve Go?", "go.problem.body": - "Ci concentriamo nel portare l'esperienza OpenCode a quante più persone possibile. OpenCode Go è un abbonamento a basso costo: $5 il primo mese, poi $10/mese. Offre limiti generosi e accesso affidabile ai modelli open source più capaci.", + "Ci concentriamo nel portare l'esperienza OpenCode a quante più persone possibile. OpenCode Go è un abbonamento a basso costo da $10/mese. Offre limiti generosi e accesso affidabile ai modelli open source più capaci.", "go.problem.subtitle": " ", "go.problem.item1": "Prezzo di abbonamento a basso costo", "go.problem.item2": "Limiti generosi e accesso affidabile", "go.problem.item3": "Costruito per il maggior numero possibile di programmatori", "go.problem.item4": "Una selezione curata di modelli testati per il coding agentico", "go.how.title": "Come funziona Go", - "go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.", + "go.how.body": "Go costa $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.", "go.how.step1.title": "Crea un account", "go.how.step1.beforeLink": "segui le", "go.how.step1.link": "istruzioni di configurazione", "go.how.step2.title": "Abbonati a Go", - "go.how.step2.link": "$5 il primo mese", - "go.how.step2.afterLink": "poi $10/mese con limiti generosi", + "go.how.step2.link": "$10/mese", + "go.how.step2.afterLink": "con limiti generosi", "go.how.step3.title": "Inizia a programmare", "go.how.step3.body": "con accesso affidabile ai modelli open source", "go.privacy.title": "La tua privacy è importante per noi", @@ -328,11 +329,11 @@ export const dict = { "go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.", "go.faq.q3": "Go è lo stesso di Zen?", "go.faq.a3": - "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e un accesso affidabile a una selezione curata di modelli.", + "No. Zen è a consumo, mentre Go costa $10/mese, con limiti generosi e un accesso affidabile a una selezione curata di modelli.", "go.faq.q4": "Quanto costa Go?", "go.faq.a4.p1.beforePricing": "Go costa", - "go.faq.a4.p1.pricingLink": "$5 il primo mese", - "go.faq.a4.p1.afterPricing": "poi $10/mese con limiti generosi.", + "go.faq.a4.p1.pricingLink": "$10/mese", + "go.faq.a4.p1.afterPricing": "con limiti generosi.", "go.faq.a4.p2.beforeAccount": "Puoi gestire il tuo abbonamento nel tuo", "go.faq.a4.p2.accountLink": "account", "go.faq.a4.p3": "Annulla in qualsiasi momento.", @@ -345,13 +346,16 @@ export const dict = { "go.faq.a5.used": "Utilizzato", "go.faq.a5.notUsed": "Non utilizzato", "go.faq.a5.noAgreement": "Nessun accordo", + "go.faq.a5.notZdr": "Non ZDR", "go.faq.a5.grokRetention": "ZDR disabilita importanti funzionalità API che dipendono dai dati archiviati, tra cui la Responses API con stato, Files and Collections e Batch API.", "go.faq.a5.gptRetention": "I log di monitoraggio degli abusi vengono generati per l'utilizzo di tutte le funzionalità API e conservati per un massimo di 30 giorni.", "go.faq.a5.learnMore": "Scopri di più", "go.faq.a5.deepseekRetention": - "L'accordo ZDR viene rinnovato mensilmente. L'accordo attuale è valido fino al 31 agosto 2026.", + "L'accordo ZDR viene rinnovato mensilmente. L'accordo attuale è valido fino al 30 settembre 2026.", + "go.faq.a5.museRetention": + "Prezzi dei token fortemente scontati in cambio dell'autorizzazione a utilizzare i tuoi prompt e completamenti per addestrare futuri modelli Meta.", "go.faq.a5.beforeExceptions": "I modelli Go sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le", "go.faq.a5.exceptionsLink": "seguenti eccezioni", @@ -365,7 +369,7 @@ export const dict = { "go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?", "go.faq.a9": - "I modelli gratuiti includono Big Pickle più i modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go offre una selezione curata di modelli con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", + "I modelli gratuiti includono Big Pickle più i modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go offre una selezione curata di modelli con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a quote base di $12 ogni 5 ore, $30 a settimana e $60 al mese; le quote specifiche possono variare in base al modello (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", "zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.", "zen.api.error.modelNotSupported": "Modello {{model}} non supportato", @@ -391,6 +395,9 @@ export const dict = { "zen.api.error.modelDisabled": "Il modello è disabilitato", "zen.api.error.regionNotAllowed": "La versione più recente di questo modello è disponibile solo con hosting in Cina e richiede un consenso esplicito: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Questo modello non è disponibile nel tuo paese.", + "zen.api.error.trainingNotAllowed": + "Questo modello raccoglie dati utilizzati per migliorarne la qualità e richiede il tuo consenso esplicito: {{consoleGoUrl}}", "zen.api.error.trialEnded": "La promozione gratuita di {{model}} è terminata. Puoi continuare a usare il modello abbonandoti a OpenCode Go - {{link}}", @@ -665,23 +672,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "pochi secondi", "workspace.lite.subscription.message": "Sei abbonato a OpenCode Go.", "workspace.lite.subscription.manage": "Gestisci Abbonamento", - "workspace.lite.subscription.rollingUsage": "Utilizzo Continuativo", + "workspace.lite.subscription.rollingUsage": "Utilizzo su 5 ore", + "workspace.lite.subscription.rollingQuota": "Quota su 5 ore", "workspace.lite.subscription.weeklyUsage": "Utilizzo Settimanale", + "workspace.lite.subscription.weeklyQuota": "Quota Settimanale", "workspace.lite.subscription.monthlyUsage": "Utilizzo Mensile", + "workspace.lite.subscription.monthlyQuota": "Quota Mensile", "workspace.lite.subscription.resetsIn": "Si resetta tra", + "workspace.lite.subscription.showDetails": "Mostra dettagli", + "workspace.lite.subscription.hideDetails": "Nascondi dettagli", + "workspace.lite.subscription.model": "Modello", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Totale", "workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo", "workspace.lite.subscription.selectProvider": 'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.', "workspace.lite.providers.title": "Provider", "workspace.lite.providers.description": "Controlla quali provider vengono usati per il routing.", + "workspace.lite.providers.allowTraining": "Consenti i modelli che si addestrano sui dati delle richieste", "workspace.lite.providers.useChina": "Abilita modelli ospitati in Cina", "workspace.lite.black.message": "Attualmente sei abbonato a OpenCode Black o sei in lista d'attesa. Annulla l'iscrizione prima se desideri passare a Go.", "workspace.lite.other.message": "Un altro membro in questo workspace è già abbonato a OpenCode Go. Solo un membro per workspace può abbonarsi.", "workspace.lite.promo.description": - "OpenCode Go parte da {{price}}, poi $10/mese, e offre un accesso affidabile a popolari modelli di coding aperti con generosi limiti di utilizzo.", - "workspace.lite.promo.price": "$5 il primo mese", + "OpenCode Go costa {{price}} e offre un accesso affidabile a popolari modelli di coding aperti con generosi limiti di utilizzo.", + "workspace.lite.promo.price": "$10/mese", "workspace.lite.promo.modelsTitle": "Cosa è incluso", "workspace.lite.promo.footer": "Il piano è pensato principalmente per gli utenti internazionali e offre un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare in base a quanto apprenderemo dall'utilizzo iniziale e dai feedback.", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index fee7662f18..a79bdd12ec 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -253,9 +253,9 @@ export const dict = { "zen.privacy.exceptionsLink": "以下の例外", "go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル", - "go.banner.text": "DeepSeek V4 Flashの利用上限が期間限定で2倍に", + "go.banner.text": "GLM-5.3-Flashの利用上限が期間限定で2倍に", "go.meta.description": - "Goは最初の月$5、その後$10/月で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。", + "Goは月額$10で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。", "go.hero.title": "すべての人のための低価格なコーディングモデル", "go.hero.body": "Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。", @@ -264,13 +264,14 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Goを購読する", "go.cta.price": "$10/月", - "go.cta.promo": "初月 $5", "go.pricing.body": - "どのエージェントでも使えます。最初の月$5、その後$10/月。必要に応じてクレジットを追加。いつでもキャンセルできます。", + "どのエージェントでも使えます。月額$10。必要に応じてクレジットを追加。いつでもキャンセルできます。", "go.graph.free": "無料", "go.graph.freePill": "Big Pickleと無料モデル", "go.graph.go": "Go", "go.graph.label": "5時間あたりのリクエスト数", + "go.graph.limitedRegions": "一部の地域に限定", + "go.graph.limitedTime": "期間限定", "go.graph.usageLimits": "利用制限", "go.graph.aria": "5時間あたりのリクエスト数: {{free}} 対 {{go}}", @@ -298,20 +299,20 @@ export const dict = { "go.testimonials.frank.quote": "まだNvidiaにいられたらよかったのに。", "go.problem.title": "Goはどのような問題を解決していますか?", "go.problem.body": - "私たちはOpenCodeの体験をできるだけ多くの人に届けることに注力しています。OpenCode Goは低価格のサブスクリプションで、最初の月は$5、その後は$10/月です。ゆとりある上限と、最も高性能なオープンソースモデルへの信頼できるアクセスを提供します。", + "私たちはOpenCodeの体験をできるだけ多くの人に届けることに注力しています。OpenCode Goは月額$10の低価格なサブスクリプションです。ゆとりある上限と、最も高性能なオープンソースモデルへの信頼できるアクセスを提供します。", "go.problem.subtitle": " ", "go.problem.item1": "低価格なサブスクリプション料金", "go.problem.item2": "十分な制限と安定したアクセス", "go.problem.item3": "できるだけ多くのプログラマーのために構築", "go.problem.item4": "エージェント型コーディング向けにテストされた厳選モデルラインナップ", "go.how.title": "Goの仕組み", - "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。", + "go.how.body": "Goは月額$10です。OpenCodeまたは任意のエージェントで使えます。", "go.how.step1.title": "アカウントを作成", "go.how.step1.beforeLink": "", "go.how.step1.link": "セットアップ手順はこちら", "go.how.step2.title": "Goを購読する", - "go.how.step2.link": "最初の月$5", - "go.how.step2.afterLink": "その後$10/月、ゆとりある上限付き", + "go.how.step2.link": "月額$10", + "go.how.step2.afterLink": "ゆとりある上限付き", "go.how.step3.title": "コーディングを開始", "go.how.step3.body": "オープンソースモデルへの安定したアクセスで", "go.privacy.title": "あなたのプライバシーは私たちにとって重要です", @@ -328,11 +329,11 @@ export const dict = { "go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。", "go.faq.q3": "GoはZenと同じですか?", "go.faq.a3": - "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で、厳選されたモデルラインナップへのゆとりある上限と安定したアクセスを提供します。", + "いいえ。Zenは従量課金制ですが、Goは月額$10で、厳選されたモデルラインナップへのゆとりある上限と安定したアクセスを提供します。", "go.faq.q4": "Goの料金は?", "go.faq.a4.p1.beforePricing": "Goは", - "go.faq.a4.p1.pricingLink": "最初の月$5", - "go.faq.a4.p1.afterPricing": "その後$10/月、ゆとりある上限付き。", + "go.faq.a4.p1.pricingLink": "月額$10", + "go.faq.a4.p1.afterPricing": "ゆとりある上限付き。", "go.faq.a4.p2.beforeAccount": "管理画面:", "go.faq.a4.p2.accountLink": "アカウント", "go.faq.a4.p3": "いつでもキャンセル可能です。", @@ -345,11 +346,14 @@ export const dict = { "go.faq.a5.used": "使用あり", "go.faq.a5.notUsed": "使用なし", "go.faq.a5.noAgreement": "契約なし", + "go.faq.a5.notZdr": "ZDRではない", "go.faq.a5.grokRetention": "ZDRでは、保存データに依存する重要なAPI機能(ステートフルなResponses API、Files and Collections、Batch APIなど)が無効になります。", "go.faq.a5.gptRetention": "不正使用監視ログはすべてのAPI機能の使用時に生成され、最大30日間保持されます。", + "go.faq.a5.museRetention": + "将来のMetaモデルのトレーニングにプロンプトと生成結果を使用する許可と引き換えに、トークン料金が大幅に割引されます。", "go.faq.a5.learnMore": "詳しく見る", - "go.faq.a5.deepseekRetention": "ZDR契約は毎月更新されます。現在の契約は2026年8月31日まで有効です。", + "go.faq.a5.deepseekRetention": "ZDR契約は毎月更新されます。現在の契約は2026年9月30日まで有効です。", "go.faq.a5.beforeExceptions": "Goのモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(", "go.faq.a5.exceptionsLink": "以下の例外", @@ -363,7 +367,7 @@ export const dict = { "go.faq.q9": "無料モデルとGoの違いは何ですか?", "go.faq.a9": - "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。Goでは厳選されたモデルラインナップを利用でき、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", + "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。Goでは厳選されたモデルラインナップを利用でき、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。基本利用枠では概算で5時間あたり$12、週間$30、月間$60相当ですが、モデル別の利用枠は異なる場合があります(実際のリクエスト数はモデルと使用状況により異なります)。", "zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。", "zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません", @@ -390,6 +394,9 @@ export const dict = { "zen.api.error.modelDisabled": "モデルが無効です", "zen.api.error.regionNotAllowed": "このモデルの最新バージョンは中国でのみホスト提供されており、利用するには明示的なオプトインが必要です: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "このモデルはお住まいの国では利用できません。", + "zen.api.error.trainingNotAllowed": + "このモデルは品質向上のためのデータを収集します。利用するには明示的な同意が必要です: {{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} の無料プロモーションは終了しました。OpenCode Go を購読するとモデルを引き続き使用できます - {{link}}", @@ -663,23 +670,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "数秒", "workspace.lite.subscription.message": "あなたは OpenCode Go を購読しています。", "workspace.lite.subscription.manage": "サブスクリプションの管理", - "workspace.lite.subscription.rollingUsage": "ローリング利用量", + "workspace.lite.subscription.rollingUsage": "5時間利用量", + "workspace.lite.subscription.rollingQuota": "5時間上限", "workspace.lite.subscription.weeklyUsage": "週間利用量", + "workspace.lite.subscription.weeklyQuota": "週間上限", "workspace.lite.subscription.monthlyUsage": "月間利用量", + "workspace.lite.subscription.monthlyQuota": "月間上限", "workspace.lite.subscription.resetsIn": "リセットまで", + "workspace.lite.subscription.showDetails": "詳細を表示", + "workspace.lite.subscription.hideDetails": "詳細を非表示", + "workspace.lite.subscription.model": "モデル", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "合計", "workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する", "workspace.lite.subscription.selectProvider": "Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。", "workspace.lite.providers.title": "プロバイダー", "workspace.lite.providers.description": "ルーティングに使用するプロバイダーを管理します。", + "workspace.lite.providers.allowTraining": "リクエストデータを学習に使用するモデルを許可", "workspace.lite.providers.useChina": "中国でホストされているモデルを有効にする", "workspace.lite.black.message": "現在 OpenCode Black を購読中、またはウェイティングリストに登録されています。Go に切り替える場合は、先に登録を解除してください。", "workspace.lite.other.message": "このワークスペースの別のメンバーが既に OpenCode Go を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。", "workspace.lite.promo.description": - "OpenCode Goは{{price}}で始まり、その後は$10/月で、人気の高いオープンコーディングモデルへの安定したアクセスと余裕のある利用枠を提供します。", - "workspace.lite.promo.price": "初月$5", + "OpenCode Goは{{price}}で、人気の高いオープンコーディングモデルへの安定したアクセスと余裕のある利用枠を提供します。", + "workspace.lite.promo.price": "$10/月", "workspace.lite.promo.modelsTitle": "含まれるもの", "workspace.lite.promo.footer": "このプランは主に海外のユーザー向けに設計されており、世界中から安定してご利用いただけます。料金と利用上限は、初期の利用状況やフィードバックを踏まえて変更される場合があります。", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index d97f87490c..72470d0330 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -250,9 +250,8 @@ export const dict = { "zen.privacy.exceptionsLink": "다음 예외", "go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델", - "go.banner.text": "DeepSeek V4 Flash 사용 한도가 한시적으로 2배 확대됩니다", - "go.meta.description": - "Go는 첫 달 $5, 이후 $10/월로 시작하며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.", + "go.banner.text": "GLM-5.3-Flash 사용 한도가 한시적으로 2배 확대됩니다", + "go.meta.description": "Go는 월 $10이며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.", "go.hero.title": "모두를 위한 저비용 코딩 모델", "go.hero.body": "Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.", @@ -261,13 +260,14 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Go 구독하기", "go.cta.price": "$10/월", - "go.cta.promo": "첫 달 $5", "go.pricing.body": - "어떤 에이전트와도 사용할 수 있습니다. 첫 달 $5, 이후 $10/월. 필요하면 크레딧을 충전하세요. 언제든지 취소할 수 있습니다.", + "어떤 에이전트와도 사용할 수 있습니다. 월 $10. 필요하면 크레딧을 충전하세요. 언제든지 취소할 수 있습니다.", "go.graph.free": "무료", "go.graph.freePill": "Big Pickle 및 무료 모델", "go.graph.go": "Go", "go.graph.label": "5시간당 요청 수", + "go.graph.limitedRegions": "일부 지역에서만 제공", + "go.graph.limitedTime": "한정된 기간", "go.graph.usageLimits": "사용 한도", "go.graph.aria": "5시간당 요청 수: {{free}} 대 {{go}}", @@ -295,20 +295,20 @@ export const dict = { "go.testimonials.frank.quote": "아직 Nvidia에 있었으면 좋았을 텐데요.", "go.problem.title": "Go는 어떤 문제를 해결하나요?", "go.problem.body": - "우리는 가능한 많은 사람들에게 OpenCode 경험을 제공하는 데 집중하고 있습니다. OpenCode Go는 저렴한 구독 서비스로, 첫 달 $5, 이후 $10/월입니다. 넉넉한 한도와 가장 뛰어난 오픈 소스 모델에 대한 안정적인 액세스를 제공합니다.", + "우리는 가능한 많은 사람들에게 OpenCode 경험을 제공하는 데 집중하고 있습니다. OpenCode Go는 월 $10의 저렴한 구독 서비스입니다. 넉넉한 한도와 가장 뛰어난 오픈 소스 모델에 대한 안정적인 액세스를 제공합니다.", "go.problem.subtitle": " ", "go.problem.item1": "저렴한 구독 가격", "go.problem.item2": "넉넉한 한도와 안정적인 액세스", "go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨", "go.problem.item4": "에이전트 코딩용으로 테스트된 엄선된 모델 라인업", "go.how.title": "Go 작동 방식", - "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", + "go.how.body": "Go는 월 $10입니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", "go.how.step1.title": "계정 생성", "go.how.step1.beforeLink": "", "go.how.step1.link": "설정 지침을 따르세요", "go.how.step2.title": "Go 구독", - "go.how.step2.link": "첫 달 $5", - "go.how.step2.afterLink": "이후 $10/월, 넉넉한 한도 포함", + "go.how.step2.link": "월 $10", + "go.how.step2.afterLink": "넉넉한 한도 포함", "go.how.step3.title": "코딩 시작", "go.how.step3.body": "오픈 소스 모델에 대한 안정적인 액세스와 함께", "go.privacy.title": "귀하의 프라이버시는 우리에게 중요합니다", @@ -324,11 +324,11 @@ export const dict = { "go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.", "go.faq.q3": "Go는 Zen과 같은가요?", "go.faq.a3": - "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, 엄선된 모델 라인업에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", + "아니요. Zen은 종량제인 반면, Go는 월 $10이며, 엄선된 모델 라인업에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", "go.faq.q4": "Go 비용은 얼마인가요?", "go.faq.a4.p1.beforePricing": "Go 비용은", - "go.faq.a4.p1.pricingLink": "첫 달 $5", - "go.faq.a4.p1.afterPricing": "이후 $10/월, 넉넉한 한도 포함.", + "go.faq.a4.p1.pricingLink": "월 $10", + "go.faq.a4.p1.afterPricing": "넉넉한 한도 포함.", "go.faq.a4.p2.beforeAccount": "구독 관리는 다음에서 가능합니다:", "go.faq.a4.p2.accountLink": "계정", "go.faq.a4.p3": "언제든지 취소할 수 있습니다.", @@ -341,11 +341,14 @@ export const dict = { "go.faq.a5.used": "사용됨", "go.faq.a5.notUsed": "사용되지 않음", "go.faq.a5.noAgreement": "합의 없음", + "go.faq.a5.notZdr": "ZDR 아님", "go.faq.a5.grokRetention": "ZDR은 저장된 데이터에 의존하는 중요한 API 기능(상태 저장형 Responses API, Files and Collections, Batch API 포함)을 비활성화합니다.", "go.faq.a5.gptRetention": "모든 API 기능 사용에 대해 악용 모니터링 로그가 생성되며 최대 30일 동안 보존됩니다.", + "go.faq.a5.museRetention": + "향후 Meta 모델 학습에 사용자의 프롬프트와 생성 결과를 사용할 수 있도록 허용하는 대신 토큰 가격이 대폭 할인됩니다.", "go.faq.a5.learnMore": "자세히 알아보기", - "go.faq.a5.deepseekRetention": "ZDR 계약은 매월 갱신됩니다. 현재 계약은 2026년 8월 31일까지 유효합니다.", + "go.faq.a5.deepseekRetention": "ZDR 계약은 매월 갱신됩니다. 현재 계약은 2026년 9월 30일까지 유효합니다.", "go.faq.a5.beforeExceptions": "Go 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,", "go.faq.a5.exceptionsLink": "다음 예외", @@ -358,7 +361,7 @@ export const dict = { "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.a9": - "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 엄선된 모델 라인업을 제공하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", + "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 엄선된 모델 라인업을 제공하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 기본 할당량은 대략 5시간당 $12, 주당 $30, 월 $60에 해당하며 모델별 할당량은 다를 수 있습니다(실제 요청 수는 모델 및 사용량에 따라 다름).", "zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.", "zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다", @@ -384,6 +387,9 @@ export const dict = { "zen.api.error.modelDisabled": "모델이 비활성화되었습니다", "zen.api.error.regionNotAllowed": "이 모델의 최신 버전은 중국에서 호스팅되는 경우에만 사용할 수 있으며, 명시적으로 사용에 동의해야 합니다: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "이 모델은 거주 국가에서 사용할 수 없습니다.", + "zen.api.error.trainingNotAllowed": + "이 모델은 품질 개선에 사용되는 데이터를 수집하며, 사용하려면 명시적으로 동의해야 합니다: {{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}}의 무료 프로모션이 종료되었습니다. OpenCode Go를 구독하면 모델을 계속 사용할 수 있습니다 - {{link}}", @@ -655,23 +661,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "몇 초", "workspace.lite.subscription.message": "현재 OpenCode Go를 구독 중입니다.", "workspace.lite.subscription.manage": "구독 관리", - "workspace.lite.subscription.rollingUsage": "롤링 사용량", + "workspace.lite.subscription.rollingUsage": "5시간 사용량", + "workspace.lite.subscription.rollingQuota": "5시간 할당량", "workspace.lite.subscription.weeklyUsage": "주간 사용량", + "workspace.lite.subscription.weeklyQuota": "주간 할당량", "workspace.lite.subscription.monthlyUsage": "월간 사용량", + "workspace.lite.subscription.monthlyQuota": "월간 할당량", "workspace.lite.subscription.resetsIn": "초기화까지 남은 시간:", + "workspace.lite.subscription.showDetails": "상세 정보 보기", + "workspace.lite.subscription.hideDetails": "상세 정보 숨기기", + "workspace.lite.subscription.model": "모델", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "합계", "workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용", "workspace.lite.subscription.selectProvider": 'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.', "workspace.lite.providers.title": "공급자", "workspace.lite.providers.description": "라우팅에 사용할 공급자를 제어합니다.", + "workspace.lite.providers.allowTraining": "요청 데이터를 학습에 사용하는 모델 허용", "workspace.lite.providers.useChina": "중국에서 호스팅되는 모델 활성화", "workspace.lite.black.message": "현재 OpenCode Black을 구독 중이거나 대기 명단에 등록되어 있습니다. Go로 전환하려면 먼저 구독을 취소해 주세요.", "workspace.lite.other.message": "이 워크스페이스의 다른 멤버가 이미 OpenCode Go를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.", "workspace.lite.promo.description": - "OpenCode Go는 {{price}}부터 시작하며, 이후 $10/월로 넉넉한 사용량 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공합니다.", - "workspace.lite.promo.price": "첫 달 $5", + "OpenCode Go는 {{price}}로 넉넉한 사용량 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공합니다.", + "workspace.lite.promo.price": "$10/월", "workspace.lite.promo.modelsTitle": "포함 내역", "workspace.lite.promo.footer": "이 플랜은 주로 해외 사용자를 위해 설계되었으며, 전 세계에서 안정적으로 이용할 수 있습니다. 초기 이용 현황과 피드백을 반영하는 과정에서 가격과 사용 한도가 변경될 수 있습니다.", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index b4e04a9236..2357766d25 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -254,9 +254,9 @@ export const dict = { "zen.privacy.exceptionsLink": "følgende unntak", "go.title": "OpenCode Go | Rimelige kodemodeller for alle", - "go.banner.text": "DeepSeek V4 Flash får 2x bruksgrense i en begrenset periode", + "go.banner.text": "GLM-5.3-Flash får 2x bruksgrense i en begrenset periode", "go.meta.description": - "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse bruksgrenser og pålitelig tilgang til ledende kodemodeller.", + "Go koster $10/måned, med sjenerøse bruksgrenser og pålitelig tilgang til ledende kodemodeller.", "go.hero.title": "Rimelige kodemodeller for alle", "go.hero.body": "Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.", @@ -265,13 +265,13 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Abonner på Go", "go.cta.price": "$10/måned", - "go.cta.promo": "$5 første måned", - "go.pricing.body": - "Bruk med hvilken som helst agent. $5 første måned, deretter $10/måned. Fyll på kreditt ved behov. Avslutt når som helst.", + "go.pricing.body": "Bruk med hvilken som helst agent. $10/måned. Fyll på kreditt ved behov. Avslutt når som helst.", "go.graph.free": "Gratis", "go.graph.freePill": "Big Pickle og gratis modeller", "go.graph.go": "Go", "go.graph.label": "Forespørsler per 5 timer", + "go.graph.limitedRegions": "begrensede regioner", + "go.graph.limitedTime": "begrenset periode", "go.graph.usageLimits": "Bruksgrenser", "go.graph.aria": "Forespørsler per 5t: {{free}} vs {{go}}", @@ -298,21 +298,20 @@ export const dict = { "go.testimonials.frank.quote": "Jeg skulle ønske jeg fortsatt var hos Nvidia.", "go.problem.title": "Hvilket problem løser Go?", "go.problem.body": - "Vi fokuserer på å bringe OpenCode-opplevelsen til så mange som mulig. OpenCode Go er et rimelig abonnement: $5 for den første måneden, deretter $10/måned. Det gir sjenerøse grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene.", + "Vi fokuserer på å bringe OpenCode-opplevelsen til så mange som mulig. OpenCode Go er et rimelig abonnement til $10/måned. Det gir sjenerøse grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene.", "go.problem.subtitle": " ", "go.problem.item1": "Rimelig abonnementspris", "go.problem.item2": "Rause grenser og pålitelig tilgang", "go.problem.item3": "Bygget for så mange programmerere som mulig", "go.problem.item4": "Et kuratert modellutvalg testet for agent-koding", "go.how.title": "Hvordan Go fungerer", - "go.how.body": - "Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.", + "go.how.body": "Go koster $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.", "go.how.step1.title": "Opprett en konto", "go.how.step1.beforeLink": "følg", "go.how.step1.link": "oppsettsinstruksjonene", "go.how.step2.title": "Abonner på Go", - "go.how.step2.link": "$5 første måned", - "go.how.step2.afterLink": "deretter $10/måned med sjenerøse grenser", + "go.how.step2.link": "$10/måned", + "go.how.step2.afterLink": "med sjenerøse grenser", "go.how.step3.title": "Begynn å kode", "go.how.step3.body": "med pålitelig tilgang til åpen kildekode-modeller", "go.privacy.title": "Personvernet ditt er viktig for oss", @@ -329,11 +328,11 @@ export const dict = { "go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til et kuratert modellutvalg.", + "Nei. Zen er betaling etter bruk, mens Go koster $10/måned, med sjenerøse grenser og pålitelig tilgang til et kuratert modellutvalg.", "go.faq.q4": "Hva koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", - "go.faq.a4.p1.pricingLink": "$5 første måned", - "go.faq.a4.p1.afterPricing": "deretter $10/måned med sjenerøse grenser.", + "go.faq.a4.p1.pricingLink": "$10/måned", + "go.faq.a4.p1.afterPricing": "med sjenerøse grenser.", "go.faq.a4.p2.beforeAccount": "Du kan administrere abonnementet ditt i din", "go.faq.a4.p2.accountLink": "konto", "go.faq.a4.p3": "Avslutt når som helst.", @@ -346,13 +345,16 @@ export const dict = { "go.faq.a5.used": "Brukes", "go.faq.a5.notUsed": "Brukes ikke", "go.faq.a5.noAgreement": "Ingen avtale", + "go.faq.a5.notZdr": "Ikke ZDR", "go.faq.a5.grokRetention": "ZDR deaktiverer viktige API-funksjoner som er avhengige av lagrede data, inkludert den tilstandsbaserte Responses API, Files and Collections og Batch API.", "go.faq.a5.gptRetention": "Logger for overvåking av misbruk genereres for all bruk av API-funksjoner og oppbevares i opptil 30 dager.", "go.faq.a5.learnMore": "Les mer", "go.faq.a5.deepseekRetention": - "ZDR-avtalen fornyes månedlig. Den gjeldende avtalen er gyldig til og med 31. august 2026.", + "ZDR-avtalen fornyes månedlig. Den gjeldende avtalen er gyldig til og med 30. september 2026.", + "go.faq.a5.museRetention": + "Kraftig rabatterte tokenpriser i bytte mot tillatelse til å bruke ledetekstene og fullføringene dine til å trene fremtidige Meta-modeller.", "go.faq.a5.beforeExceptions": "Go-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med", @@ -367,7 +369,7 @@ export const dict = { "go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller som er tilgjengelige på det tidspunktet, med en kvote på 200 forespørsler/dag. Go tilbyr et kuratert modellutvalg med høyere forespørselskvoter som håndheves over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", + "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller som er tilgjengelige på det tidspunktet, med en kvote på 200 forespørsler/dag. Go tilbyr et kuratert modellutvalg med høyere forespørselskvoter som håndheves over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende basiskvoter på $12 per 5 timer, $30 per uke og $60 per måned; modellspesifikke kvoter kan variere (faktiske forespørselsantall varierer etter modell og bruk).", "zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.", "zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke", @@ -393,6 +395,9 @@ export const dict = { "zen.api.error.modelDisabled": "Modellen er deaktivert", "zen.api.error.regionNotAllowed": "Den nyeste versjonen av denne modellen er bare tilgjengelig som en tjeneste driftet i Kina og krever at du uttrykkelig samtykker: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Denne modellen er ikke tilgjengelig i landet ditt.", + "zen.api.error.trainingNotAllowed": + "Denne modellen samler inn data som brukes til å forbedre modellens kvalitet, og krever at du uttrykkelig samtykker: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Den gratis kampanjen for {{model}} er avsluttet. Du kan fortsette å bruke modellen ved å abonnere på OpenCode Go - {{link}}", @@ -665,23 +670,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "noen få sekunder", "workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.", "workspace.lite.subscription.manage": "Administrer abonnement", - "workspace.lite.subscription.rollingUsage": "Løpende bruk", + "workspace.lite.subscription.rollingUsage": "5-timers bruk", + "workspace.lite.subscription.rollingQuota": "5-timers kvote", "workspace.lite.subscription.weeklyUsage": "Ukentlig bruk", + "workspace.lite.subscription.weeklyQuota": "Ukentlig kvote", "workspace.lite.subscription.monthlyUsage": "Månedlig bruk", + "workspace.lite.subscription.monthlyQuota": "Månedlig kvote", "workspace.lite.subscription.resetsIn": "Nullstilles om", + "workspace.lite.subscription.showDetails": "Vis detaljer", + "workspace.lite.subscription.hideDetails": "Skjul detaljer", + "workspace.lite.subscription.model": "Modell", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Totalt", "workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene", "workspace.lite.subscription.selectProvider": 'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.', "workspace.lite.providers.title": "Leverandører", "workspace.lite.providers.description": "Kontroller hvilke leverandører som brukes til ruting.", + "workspace.lite.providers.allowTraining": "Tillat modeller som trener på forespørselsdata", "workspace.lite.providers.useChina": "Aktiver modeller hostet i Kina", "workspace.lite.black.message": "Du abonnerer for øyeblikket på OpenCode Black eller står på venteliste. Vennligst avslutt abonnementet først hvis du vil bytte til Go.", "workspace.lite.other.message": "Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Go. Kun ett medlem per arbeidsområde kan abonnere.", "workspace.lite.promo.description": - "OpenCode Go starter på {{price}}, deretter $10/måned, og gir pålitelig tilgang til populære åpne kodingsmodeller med sjenerøse bruksgrenser.", - "workspace.lite.promo.price": "$5 for den første måneden", + "OpenCode Go koster {{price}} og gir pålitelig tilgang til populære åpne kodingsmodeller med sjenerøse bruksgrenser.", + "workspace.lite.promo.price": "$10/måned", "workspace.lite.promo.modelsTitle": "Hva som er inkludert", "workspace.lite.promo.footer": "Planen er primært utviklet for internasjonale brukere og gir stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer av tidlig bruk og tilbakemeldinger.", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 375d8e3e6f..e2bb3d8116 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -255,9 +255,9 @@ export const dict = { "zen.privacy.exceptionsLink": "następującymi wyjątkami", "go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego", - "go.banner.text": "DeepSeek V4 Flash oferuje 2x wyższe limity użycia przez ograniczony czas", + "go.banner.text": "GLM-5.3-Flash oferuje 2x wyższe limity użycia przez ograniczony czas", "go.meta.description": - "Go kosztuje $5 za pierwszy miesiąc, a następnie $10/miesiąc, oferując hojne limity użycia i niezawodny dostęp do wiodących modeli do kodowania.", + "Go kosztuje $10/miesiąc, oferując hojne limity użycia i niezawodny dostęp do wiodących modeli do kodowania.", "go.hero.title": "Niskokosztowe modele do kodowania dla każdego", "go.hero.body": "Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.", @@ -266,13 +266,14 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Zasubskrybuj Go", "go.cta.price": "$10/miesiąc", - "go.cta.promo": "$5 pierwszy miesiąc", "go.pricing.body": - "Używaj z dowolnym agentem. $5 za pierwszy miesiąc, potem $10/miesiąc. Doładuj konto w razie potrzeby. Anuluj w dowolnym momencie.", + "Używaj z dowolnym agentem. $10/miesiąc. Doładuj konto w razie potrzeby. Anuluj w dowolnym momencie.", "go.graph.free": "Darmowe", "go.graph.freePill": "Big Pickle i darmowe modele", "go.graph.go": "Go", "go.graph.label": "Żądania na 5 godzin", + "go.graph.limitedRegions": "ograniczone regiony", + "go.graph.limitedTime": "ograniczony czas", "go.graph.usageLimits": "Limity użycia", "go.graph.aria": "Żądania na 5h: {{free}} vs {{go}}", @@ -299,21 +300,20 @@ export const dict = { "go.testimonials.frank.quote": "Chciałbym wciąż być w Nvidia.", "go.problem.title": "Jaki problem rozwiązuje Go?", "go.problem.body": - "Skupiamy się na udostępnieniu doświadczenia OpenCode jak największej liczbie osób. OpenCode Go to tania subskrypcja: $5 za pierwszy miesiąc, potem $10/miesiąc. Zapewnia hojne limity i niezawodny dostęp do najbardziej wydajnych modeli open source.", + "Skupiamy się na udostępnieniu doświadczenia OpenCode jak największej liczbie osób. OpenCode Go to tania subskrypcja za $10/miesiąc. Zapewnia hojne limity i niezawodny dostęp do najbardziej wydajnych modeli open source.", "go.problem.subtitle": " ", "go.problem.item1": "Niskokosztowa cena subskrypcji", "go.problem.item2": "Hojne limity i niezawodny dostęp", "go.problem.item3": "Stworzony dla jak największej liczby programistów", "go.problem.item4": "Starannie dobrany zestaw modeli przetestowanych pod kątem kodowania z agentami", "go.how.title": "Jak działa Go", - "go.how.body": - "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.", + "go.how.body": "Go kosztuje $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.", "go.how.step1.title": "Załóż konto", "go.how.step1.beforeLink": "postępuj zgodnie z", "go.how.step1.link": "instrukcją konfiguracji", "go.how.step2.title": "Zasubskrybuj Go", - "go.how.step2.link": "$5 za pierwszy miesiąc", - "go.how.step2.afterLink": "potem $10/miesiąc z hojnymi limitami", + "go.how.step2.link": "$10/miesiąc", + "go.how.step2.afterLink": "z hojnymi limitami", "go.how.step3.title": "Zacznij kodować", "go.how.step3.body": "z niezawodnym dostępem do modeli open source", "go.privacy.title": "Twoja prywatność jest dla nas ważna", @@ -330,11 +330,11 @@ export const dict = { "go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.", "go.faq.q3": "Czy Go to to samo co Zen?", "go.faq.a3": - "Nie. Zen działa w modelu płatności za użycie, natomiast Go kosztuje $5 za pierwszy miesiąc, a następnie $10/miesiąc, oferując hojne limity i niezawodny dostęp do starannie dobranego zestawu modeli.", + "Nie. Zen działa w modelu płatności za użycie, natomiast Go kosztuje $10/miesiąc, oferując hojne limity i niezawodny dostęp do starannie dobranego zestawu modeli.", "go.faq.q4": "Ile kosztuje Go?", "go.faq.a4.p1.beforePricing": "Go kosztuje", - "go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc", - "go.faq.a4.p1.afterPricing": "potem $10/miesiąc z hojnymi limitami.", + "go.faq.a4.p1.pricingLink": "$10/miesiąc", + "go.faq.a4.p1.afterPricing": "z hojnymi limitami.", "go.faq.a4.p2.beforeAccount": "Możesz zarządzać subskrypcją na swoim", "go.faq.a4.p2.accountLink": "koncie", "go.faq.a4.p3": "Anuluj w dowolnym momencie.", @@ -347,12 +347,15 @@ export const dict = { "go.faq.a5.used": "Wykorzystywane", "go.faq.a5.notUsed": "Niewykorzystywane", "go.faq.a5.noAgreement": "Brak umowy", + "go.faq.a5.notZdr": "Nie ZDR", "go.faq.a5.grokRetention": "ZDR wyłącza ważne funkcje API zależne od przechowywanych danych, w tym stanowy Responses API, Files and Collections oraz Batch API.", "go.faq.a5.gptRetention": "Dzienniki monitorowania nadużyć są generowane dla każdego użycia funkcji API i przechowywane przez maksymalnie 30 dni.", "go.faq.a5.learnMore": "Dowiedz się więcej", - "go.faq.a5.deepseekRetention": "Umowa ZDR jest odnawiana co miesiąc. Obecna umowa obowiązuje do 31 sierpnia 2026 r.", + "go.faq.a5.deepseekRetention": "Umowa ZDR jest odnawiana co miesiąc. Obecna umowa obowiązuje do 30 września 2026 r.", + "go.faq.a5.museRetention": + "Znacznie obniżone ceny tokenów w zamian za zgodę na wykorzystanie Twoich promptów i odpowiedzi do trenowania przyszłych modeli Meta.", "go.faq.a5.beforeExceptions": "Modele Go są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie używają Twoich danych do trenowania modeli, z", @@ -367,7 +370,7 @@ export const dict = { "go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?", "go.faq.a9": - "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go oferuje starannie dobrany zestaw modeli z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), odpowiadającymi w przybliżeniu $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", + "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go oferuje starannie dobrany zestaw modeli z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), odpowiadającymi w przybliżeniu bazowym limitom $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie; limity mogą się różnić zależnie od modelu (rzeczywista liczba zapytań zależy od modelu i użycia).", "zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.", "zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany", @@ -393,6 +396,9 @@ export const dict = { "zen.api.error.modelDisabled": "Model jest wyłączony", "zen.api.error.regionNotAllowed": "Najnowsza wersja tego modelu jest dostępna wyłącznie jako usługa hostowana w Chinach i wymaga jawnej zgody użytkownika: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Ten model nie jest dostępny w Twoim kraju.", + "zen.api.error.trainingNotAllowed": + "Ten model zbiera dane wykorzystywane do poprawy jego jakości i wymaga wyraźnej zgody użytkownika: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Bezpłatna promocja {{model}} dobiegła końca. Możesz dalej korzystać z modelu, subskrybując OpenCode Go - {{link}}", @@ -665,23 +671,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "kilka sekund", "workspace.lite.subscription.message": "Subskrybujesz OpenCode Go.", "workspace.lite.subscription.manage": "Zarządzaj subskrypcją", - "workspace.lite.subscription.rollingUsage": "Użycie kroczące", + "workspace.lite.subscription.rollingUsage": "Użycie w ciągu 5 godzin", + "workspace.lite.subscription.rollingQuota": "Limit 5-godzinny", "workspace.lite.subscription.weeklyUsage": "Użycie tygodniowe", + "workspace.lite.subscription.weeklyQuota": "Limit tygodniowy", "workspace.lite.subscription.monthlyUsage": "Użycie miesięczne", + "workspace.lite.subscription.monthlyQuota": "Limit miesięczny", "workspace.lite.subscription.resetsIn": "Resetuje się za", + "workspace.lite.subscription.showDetails": "Pokaż szczegóły", + "workspace.lite.subscription.hideDetails": "Ukryj szczegóły", + "workspace.lite.subscription.model": "Model", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Łącznie", "workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia", "workspace.lite.subscription.selectProvider": 'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.', "workspace.lite.providers.title": "Dostawcy", "workspace.lite.providers.description": "Kontroluj, którzy dostawcy są używani do routingu.", + "workspace.lite.providers.allowTraining": "Zezwalaj na modele wykorzystujące dane żądań do trenowania", "workspace.lite.providers.useChina": "Włącz modele hostowane w Chinach", "workspace.lite.black.message": "Obecnie subskrybujesz OpenCode Black lub jesteś na liście oczekujących. Jeśli chcesz przejść na Go, najpierw anuluj subskrypcję.", "workspace.lite.other.message": "Inny członek tego obszaru roboczego już subskrybuje OpenCode Go. Tylko jeden członek na obszar roboczy może subskrybować.", "workspace.lite.promo.description": - "OpenCode Go zaczyna się od {{price}}, potem $10/miesiąc, i zapewnia niezawodny dostęp do popularnych otwartych modeli kodowania z hojnymi limitami użycia.", - "workspace.lite.promo.price": "$5 za pierwszy miesiąc", + "OpenCode Go kosztuje {{price}} i zapewnia niezawodny dostęp do popularnych otwartych modeli kodowania z hojnymi limitami użycia.", + "workspace.lite.promo.price": "$10/miesiąc", "workspace.lite.promo.modelsTitle": "Co zawiera", "workspace.lite.promo.footer": "Plan został opracowany przede wszystkim z myślą o użytkownikach z całego świata i zapewnia stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę zdobywania doświadczeń na podstawie początkowego korzystania z usługi i otrzymywanych opinii.", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index bda7404131..9e05c95491 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -258,9 +258,9 @@ export const dict = { "zen.privacy.exceptionsLink": "следующими исключениями", "go.title": "OpenCode Go | Недорогие модели для кодинга для всех", - "go.banner.text": "DeepSeek V4 Flash получает 2x лимиты использования на ограниченное время", + "go.banner.text": "GLM-5.3-Flash получает 2x лимиты использования на ограниченное время", "go.meta.description": - "Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.", + "Go стоит $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.", "go.hero.title": "Недорогие модели для кодинга для всех", "go.hero.body": "Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.", @@ -269,13 +269,14 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Подписаться на Go", "go.cta.price": "$10/месяц", - "go.cta.promo": "$5 первый месяц", "go.pricing.body": - "Используйте с любым агентом. $5 за первый месяц, затем $10/месяц. Пополняйте баланс при необходимости. Отменить можно в любое время.", + "Используйте с любым агентом. $10/месяц. Пополняйте баланс при необходимости. Отменить можно в любое время.", "go.graph.free": "Бесплатно", "go.graph.freePill": "Big Pickle и бесплатные модели", "go.graph.go": "Go", "go.graph.label": "Запросов за 5 часов", + "go.graph.limitedRegions": "доступно в отдельных регионах", + "go.graph.limitedTime": "ограниченное время", "go.graph.usageLimits": "Лимиты использования", "go.graph.aria": "Запросов за 5ч: {{free}} против {{go}}", @@ -303,21 +304,20 @@ export const dict = { "go.testimonials.frank.quote": "Жаль, что я больше не в Nvidia.", "go.problem.title": "Какую проблему решает Go?", "go.problem.body": - "Мы стремимся сделать OpenCode доступным для как можно большего числа людей. OpenCode Go - это недорогая подписка: $5 за первый месяц, затем $10/месяц. Она предоставляет щедрые лимиты и надежный доступ к самым мощным моделям с открытым исходным кодом.", + "Мы стремимся сделать OpenCode доступным для как можно большего числа людей. OpenCode Go - это недорогая подписка за $10/месяц. Она предоставляет щедрые лимиты и надежный доступ к самым мощным моделям с открытым исходным кодом.", "go.problem.subtitle": " ", "go.problem.item1": "Недорогая подписка", "go.problem.item2": "Щедрые лимиты и надежный доступ", "go.problem.item3": "Создан для максимального числа программистов", "go.problem.item4": "Отобранные модели, протестированные для агентного программирования", "go.how.title": "Как работает Go", - "go.how.body": - "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", + "go.how.body": "Go стоит $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", "go.how.step1.title": "Создайте аккаунт", "go.how.step1.beforeLink": "следуйте", "go.how.step1.link": "инструкциям по настройке", "go.how.step2.title": "Подпишитесь на Go", - "go.how.step2.link": "$5 за первый месяц", - "go.how.step2.afterLink": "затем $10/месяц с щедрыми лимитами", + "go.how.step2.link": "$10/месяц", + "go.how.step2.afterLink": "с щедрыми лимитами", "go.how.step3.title": "Начните кодить", "go.how.step3.body": "с надежным доступом к open-source моделям", "go.privacy.title": "Ваша приватность важна для нас", @@ -334,11 +334,11 @@ export const dict = { "go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.", "go.faq.q3": "Go — это то же самое, что и Zen?", "go.faq.a3": - "Нет. Zen оплачивается по мере использования, а Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты и надежный доступ к отобранным моделям.", + "Нет. Zen оплачивается по мере использования, а Go стоит $10/месяц и предлагает щедрые лимиты и надежный доступ к отобранным моделям.", "go.faq.q4": "Сколько стоит Go?", "go.faq.a4.p1.beforePricing": "Go стоит", - "go.faq.a4.p1.pricingLink": "$5 за первый месяц", - "go.faq.a4.p1.afterPricing": "затем $10/месяц с щедрыми лимитами.", + "go.faq.a4.p1.pricingLink": "$10/месяц", + "go.faq.a4.p1.afterPricing": "с щедрыми лимитами.", "go.faq.a4.p2.beforeAccount": "Вы можете управлять подпиской в своем", "go.faq.a4.p2.accountLink": "аккаунте", "go.faq.a4.p3": "Отмена в любое время.", @@ -351,13 +351,16 @@ export const dict = { "go.faq.a5.used": "Используется", "go.faq.a5.notUsed": "Не используется", "go.faq.a5.noAgreement": "Нет соглашения", + "go.faq.a5.notZdr": "Не ZDR", "go.faq.a5.grokRetention": "ZDR отключает важные функции API, зависящие от сохраненных данных, включая Responses API с сохранением состояния, Files and Collections и Batch API.", "go.faq.a5.gptRetention": "Журналы мониторинга злоупотреблений создаются при любом использовании функций API и хранятся до 30 дней.", "go.faq.a5.learnMore": "Подробнее", "go.faq.a5.deepseekRetention": - "Соглашение ZDR продлевается ежемесячно. Текущее соглашение действует до 31 августа 2026 года.", + "Соглашение ZDR продлевается ежемесячно. Текущее соглашение действует до 30 сентября 2026 года.", + "go.faq.a5.museRetention": + "Значительно сниженная стоимость токенов в обмен на разрешение использовать ваши промпты и ответы для обучения будущих моделей Meta.", "go.faq.a5.beforeExceptions": "Модели Go размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за", @@ -372,7 +375,7 @@ export const dict = { "go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.a9": - "Бесплатные модели включают Big Pickle и доступные на данный момент промо-модели с квотой 200 запросов/день. Go предлагает набор отобранных моделей с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", + "Бесплатные модели включают Big Pickle и доступные на данный момент промо-модели с квотой 200 запросов/день. Go предлагает набор отобранных моделей с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно базовым лимитам $12 за 5 часов, $30 в неделю и $60 в месяц; лимиты для отдельных моделей могут отличаться (фактическое количество запросов зависит от модели и использования).", "zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.", "zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается", @@ -398,6 +401,9 @@ export const dict = { "zen.api.error.modelDisabled": "Модель отключена", "zen.api.error.regionNotAllowed": "Последняя версия этой модели размещена только в Китае. Чтобы использовать её, необходимо явно подтвердить согласие: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Эта модель недоступна в вашей стране.", + "zen.api.error.trainingNotAllowed": + "Эта модель собирает данные, которые используются для улучшения её качества, и требует явного согласия: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Бесплатная акция для {{model}} завершена. Вы можете продолжить использование модели, подписавшись на OpenCode Go - {{link}}", @@ -672,23 +678,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "несколько секунд", "workspace.lite.subscription.message": "Вы подписаны на OpenCode Go.", "workspace.lite.subscription.manage": "Управление подпиской", - "workspace.lite.subscription.rollingUsage": "Скользящее использование", + "workspace.lite.subscription.rollingUsage": "Использование за 5 часов", + "workspace.lite.subscription.rollingQuota": "Квота на 5 часов", "workspace.lite.subscription.weeklyUsage": "Недельное использование", + "workspace.lite.subscription.weeklyQuota": "Недельная квота", "workspace.lite.subscription.monthlyUsage": "Ежемесячное использование", + "workspace.lite.subscription.monthlyQuota": "Ежемесячная квота", "workspace.lite.subscription.resetsIn": "Сброс через", + "workspace.lite.subscription.showDetails": "Показать подробности", + "workspace.lite.subscription.hideDetails": "Скрыть подробности", + "workspace.lite.subscription.model": "Модель", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Итого", "workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов", "workspace.lite.subscription.selectProvider": 'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.', "workspace.lite.providers.title": "Провайдеры", "workspace.lite.providers.description": "Управляйте провайдерами, используемыми для маршрутизации.", + "workspace.lite.providers.allowTraining": "Разрешить модели, обучающиеся на данных запросов", "workspace.lite.providers.useChina": "Включить модели, размещенные в Китае", "workspace.lite.black.message": "Вы подписаны на OpenCode Black или находитесь в списке ожидания. Пожалуйста, сначала отмените подписку, если хотите перейти на Go.", "workspace.lite.other.message": "Другой участник в этом рабочем пространстве уже подписан на OpenCode Go. Только один участник в рабочем пространстве может оформить подписку.", "workspace.lite.promo.description": - "OpenCode Go начинается с {{price}}, затем $10/месяц и предоставляет надежный доступ к популярным открытым моделям кодирования с щедрыми лимитами использования.", - "workspace.lite.promo.price": "$5 за первый месяц", + "OpenCode Go стоит {{price}} и предоставляет надежный доступ к популярным открытым моделям кодирования с щедрыми лимитами использования.", + "workspace.lite.promo.price": "$10/месяц", "workspace.lite.promo.modelsTitle": "Что включено", "workspace.lite.promo.footer": "План предназначен в первую очередь для пользователей по всему миру и обеспечивает стабильный глобальный доступ. Цены и лимиты использования могут меняться по мере изучения первых результатов использования и отзывов.", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 4501b67053..56a45ba7da 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -253,9 +253,9 @@ export const dict = { "zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้", "go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", - "go.banner.text": "DeepSeek V4 Flash เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด", + "go.banner.text": "GLM-5.3-Flash เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด", "go.meta.description": - "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้", + "Go มีราคา $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้", "go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.hero.body": "Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน", @@ -264,12 +264,13 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "สมัครสมาชิก Go", "go.cta.price": "$10/เดือน", - "go.cta.promo": "$5 เดือนแรก", - "go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ $5 ในเดือนแรก จากนั้น $10/เดือน เติมเครดิตหากจำเป็น ยกเลิกได้ตลอดเวลา", + "go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ $10/เดือน เติมเครดิตหากจำเป็น ยกเลิกได้ตลอดเวลา", "go.graph.free": "ฟรี", "go.graph.freePill": "Big Pickle และโมเดลฟรี", "go.graph.go": "Go", "go.graph.label": "คำขอต่อ 5 ชั่วโมง", + "go.graph.limitedRegions": "เฉพาะบางภูมิภาค", + "go.graph.limitedTime": "ช่วงเวลาจำกัด", "go.graph.usageLimits": "ขีดจำกัดการใช้งาน", "go.graph.aria": "คำขอต่อ 5 ชม.: {{free}} vs {{go}}", @@ -296,20 +297,20 @@ export const dict = { "go.testimonials.frank.quote": "ผมหวังว่าผมจะยังอยู่ที่ Nvidia", "go.problem.title": "Go แก้ปัญหาอะไร?", "go.problem.body": - "เรามุ่งมั่นที่จะนำประสบการณ์ OpenCode ไปสู่ผู้คนให้ได้มากที่สุด OpenCode Go เป็นการสมัครสมาชิกราคาประหยัด: $5 สำหรับเดือนแรก จากนั้น $10/เดือน โดยมอบขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดอย่างเชื่อถือได้", + "เรามุ่งมั่นที่จะนำประสบการณ์ OpenCode ไปสู่ผู้คนให้ได้มากที่สุด OpenCode Go เป็นการสมัครสมาชิกราคาประหยัด $10/เดือน โดยมอบขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดอย่างเชื่อถือได้", "go.problem.subtitle": " ", "go.problem.item1": "ราคาการสมัครสมาชิกที่ต่ำ", "go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้", "go.problem.item4": "ชุดโมเดลที่คัดสรรและผ่านการทดสอบสำหรับการเขียนโค้ดแบบเอเจนต์", "go.how.title": "Go ทำงานอย่างไร", - "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", + "go.how.body": "Go มีราคา $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", "go.how.step1.title": "สร้างบัญชี", "go.how.step1.beforeLink": "ทำตาม", "go.how.step1.link": "คำแนะนำการตั้งค่า", "go.how.step2.title": "สมัครสมาชิก Go", - "go.how.step2.link": "$5 เดือนแรก", - "go.how.step2.afterLink": "จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อ", + "go.how.step2.link": "$10/เดือน", + "go.how.step2.afterLink": "พร้อมขีดจำกัดที่เอื้อเฟื้อ", "go.how.step3.title": "เริ่มเขียนโค้ด", "go.how.step3.body": "ด้วยการเข้าถึงโมเดลโอเพนซอร์สที่เชื่อถือได้", "go.privacy.title": "ความเป็นส่วนตัวของคุณสำคัญสำหรับเรา", @@ -326,11 +327,11 @@ export const dict = { "go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้", "go.faq.q3": "Go เหมือนกับ Zen หรือไม่?", "go.faq.a3": - "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงชุดโมเดลที่คัดสรรอย่างเชื่อถือได้", + "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ขณะที่ Go มีราคา $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงชุดโมเดลที่คัดสรรอย่างเชื่อถือได้", "go.faq.q4": "Go ราคาเท่าไหร่?", "go.faq.a4.p1.beforePricing": "Go ราคา", - "go.faq.a4.p1.pricingLink": "$5 เดือนแรก", - "go.faq.a4.p1.afterPricing": "จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อ", + "go.faq.a4.p1.pricingLink": "$10/เดือน", + "go.faq.a4.p1.afterPricing": "พร้อมขีดจำกัดที่เอื้อเฟื้อ", "go.faq.a4.p2.beforeAccount": "คุณสามารถจัดการการสมัครสมาชิกของคุณได้ใน", "go.faq.a4.p2.accountLink": "บัญชีของคุณ", "go.faq.a4.p3": "ยกเลิกได้ตลอดเวลา", @@ -343,12 +344,15 @@ export const dict = { "go.faq.a5.used": "นำไปใช้", "go.faq.a5.notUsed": "ไม่นำไปใช้", "go.faq.a5.noAgreement": "ไม่มีข้อตกลง", + "go.faq.a5.notZdr": "ไม่ใช่ ZDR", "go.faq.a5.grokRetention": "ZDR ปิดใช้งานฟีเจอร์ API สำคัญที่ต้องอาศัยข้อมูลที่จัดเก็บไว้ ซึ่งรวมถึง Responses API แบบมีสถานะ, Files and Collections และ Batch API", "go.faq.a5.gptRetention": "ระบบจะสร้างบันทึกการตรวจสอบการใช้งานในทางที่ผิดสำหรับการใช้งานฟีเจอร์ API ทั้งหมด และเก็บรักษาไว้นานสูงสุด 30 วัน", + "go.faq.a5.museRetention": + "ราคาของ token ลดลงอย่างมาก โดยแลกกับการอนุญาตให้นำพรอมต์และผลลัพธ์ที่สร้างขึ้นของคุณไปใช้ฝึกโมเดล Meta ในอนาคต", "go.faq.a5.learnMore": "ดูข้อมูลเพิ่มเติม", - "go.faq.a5.deepseekRetention": "ข้อตกลง ZDR จะต่ออายุทุกเดือน ข้อตกลงปัจจุบันมีผลใช้ถึงวันที่ 31 สิงหาคม 2026", + "go.faq.a5.deepseekRetention": "ข้อตกลง ZDR จะต่ออายุทุกเดือน ข้อตกลงปัจจุบันมีผลใช้ถึงวันที่ 30 กันยายน 2026", "go.faq.a5.beforeExceptions": "โมเดล Go โฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี", @@ -362,7 +366,7 @@ export const dict = { "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.a9": - "โมเดลฟรีประกอบด้วย Big Pickle และโมเดลโปรโมชันที่มีให้บริการในขณะนั้น โดยมีโควตา 200 คำขอ/วัน Go นำเสนอชุดโมเดลที่คัดสรร พร้อมโควตาคำขอที่สูงกว่าซึ่งบังคับใช้ตามกรอบเวลาแบบต่อเนื่อง (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงแตกต่างกันไปตามโมเดลและการใช้งาน)", + "โมเดลฟรีประกอบด้วย Big Pickle และโมเดลโปรโมชันที่มีให้บริการในขณะนั้น โดยมีโควตา 200 คำขอ/วัน Go นำเสนอชุดโมเดลที่คัดสรร พร้อมโควตาคำขอที่สูงกว่าซึ่งบังคับใช้ตามกรอบเวลาแบบต่อเนื่อง (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าโควตาพื้นฐานประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน โดยโควตาเฉพาะอาจแตกต่างกันไปตามโมเดล (จำนวนคำขอจริงแตกต่างกันไปตามโมเดลและการใช้งาน)", "zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง", "zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}", @@ -388,6 +392,9 @@ export const dict = { "zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน", "zen.api.error.regionNotAllowed": "โมเดลเวอร์ชันล่าสุดนี้ให้บริการเฉพาะผ่านระบบที่โฮสต์ในประเทศจีน และต้องให้ความยินยอมอย่างชัดแจ้งก่อนใช้งาน: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "โมเดลนี้ไม่พร้อมให้บริการในประเทศของคุณ", + "zen.api.error.trainingNotAllowed": + "โมเดลนี้เก็บรวบรวมข้อมูลที่ใช้เพื่อปรับปรุงคุณภาพของโมเดล และต้องได้รับความยินยอมอย่างชัดแจ้งก่อนใช้งาน: {{consoleGoUrl}}", "zen.api.error.trialEnded": "โปรโมชันฟรีสำหรับ {{model}} สิ้นสุดแล้ว คุณสามารถใช้โมเดลต่อได้โดยสมัครสมาชิก OpenCode Go - {{link}}", @@ -660,23 +667,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "ไม่กี่วินาที", "workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Go แล้ว", "workspace.lite.subscription.manage": "จัดการการสมัครสมาชิก", - "workspace.lite.subscription.rollingUsage": "การใช้งานแบบหมุนเวียน", + "workspace.lite.subscription.rollingUsage": "การใช้งานใน 5 ชั่วโมง", + "workspace.lite.subscription.rollingQuota": "โควตา 5 ชั่วโมง", "workspace.lite.subscription.weeklyUsage": "การใช้งานรายสัปดาห์", + "workspace.lite.subscription.weeklyQuota": "โควตารายสัปดาห์", "workspace.lite.subscription.monthlyUsage": "การใช้งานรายเดือน", + "workspace.lite.subscription.monthlyQuota": "โควตารายเดือน", "workspace.lite.subscription.resetsIn": "รีเซ็ตใน", + "workspace.lite.subscription.showDetails": "แสดงรายละเอียด", + "workspace.lite.subscription.hideDetails": "ซ่อนรายละเอียด", + "workspace.lite.subscription.model": "โมเดล", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "รวม", "workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน", "workspace.lite.subscription.selectProvider": 'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go', "workspace.lite.providers.title": "ผู้ให้บริการ", "workspace.lite.providers.description": "ควบคุมผู้ให้บริการที่ใช้สำหรับการกำหนดเส้นทาง", + "workspace.lite.providers.allowTraining": "อนุญาตโมเดลที่ฝึกด้วยข้อมูลคำขอ", "workspace.lite.providers.useChina": "เปิดใช้โมเดลที่โฮสต์ในจีน", "workspace.lite.black.message": "ขณะนี้คุณสมัครสมาชิก OpenCode Black หรืออยู่ในรายการรอ โปรดยกเลิกการสมัครก่อนหากต้องการเปลี่ยนไปใช้ Go", "workspace.lite.other.message": "สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Go แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น", "workspace.lite.promo.description": - "OpenCode Go เริ่มต้นที่ {{price}} จากนั้น $10/เดือน และมอบการเข้าถึงโมเดลการเขียนโค้ดแบบเปิดยอดนิยมอย่างเสถียรพร้อมขีดจำกัดการใช้งานที่ให้มาอย่างเหลือเฟือ", - "workspace.lite.promo.price": "$5 สำหรับเดือนแรก", + "OpenCode Go ราคา {{price}} และมอบการเข้าถึงโมเดลการเขียนโค้ดแบบเปิดยอดนิยมอย่างเสถียรพร้อมขีดจำกัดการใช้งานที่ให้มาอย่างเหลือเฟือ", + "workspace.lite.promo.price": "$10/เดือน", "workspace.lite.promo.modelsTitle": "สิ่งที่รวมอยู่ด้วย", "workspace.lite.promo.footer": "แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลักและให้การเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจเปลี่ยนแปลงได้ตามสิ่งที่เราเรียนรู้จากการใช้งานและข้อเสนอแนะในช่วงแรก", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 76a4926fa2..09014d0b6a 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -256,9 +256,9 @@ export const dict = { "zen.privacy.exceptionsLink": "aşağıdaki istisnalar", "go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri", - "go.banner.text": "DeepSeek V4 Flash sınırlı bir süre için 2x kullanım limiti sunuyor", + "go.banner.text": "GLM-5.3-Flash sınırlı bir süre için 2x kullanım limiti sunuyor", "go.meta.description": - "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; cömert kullanım limitleri ve önde gelen kodlama modellerine güvenilir erişim sunar.", + "Go ayda 10$'dır; cömert kullanım limitleri ve önde gelen kodlama modellerine güvenilir erişim sunar.", "go.hero.title": "Herkes için düşük maliyetli kodlama modelleri", "go.hero.body": "Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.", @@ -267,13 +267,13 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Go'ya abone ol", "go.cta.price": "Ayda 10$", - "go.cta.promo": "İlk ay $5", - "go.pricing.body": - "Herhangi bir ajanla kullanın. İlk ay $5, sonrasında ayda 10$. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.", + "go.pricing.body": "Herhangi bir ajanla kullanın. Ayda 10$. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.", "go.graph.free": "Ücretsiz", "go.graph.freePill": "Big Pickle ve ücretsiz modeller", "go.graph.go": "Go", "go.graph.label": "5 saat başına istekler", + "go.graph.limitedRegions": "sınırlı bölgeler", + "go.graph.limitedTime": "sınırlı süre", "go.graph.usageLimits": "Kullanım limitleri", "go.graph.aria": "5 saatlik istekler: {{free}} vs {{go}}", @@ -301,21 +301,20 @@ export const dict = { "go.testimonials.frank.quote": "Keşke hala Nvidia'da olsaydım.", "go.problem.title": "Go hangi sorunu çözüyor?", "go.problem.body": - "OpenCode deneyimini mümkün olduğunca çok kişiye ulaştırmaya odaklandık. OpenCode Go düşük maliyetli bir aboneliktir: İlk ay $5, sonrasında ayda 10$. Cömert limitler ve en yetenekli açık kaynak modellere güvenilir erişim sağlar.", + "OpenCode deneyimini mümkün olduğunca çok kişiye ulaştırmaya odaklandık. OpenCode Go, ayda 10$ olan düşük maliyetli bir aboneliktir. Cömert limitler ve en yetenekli açık kaynak modellere güvenilir erişim sağlar.", "go.problem.subtitle": " ", "go.problem.item1": "Düşük maliyetli abonelik fiyatlandırması", "go.problem.item2": "Cömert limitler ve güvenilir erişim", "go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi", "go.problem.item4": "Ajan tabanlı kodlama için test edilmiş, özenle seçilmiş model seçenekleri", "go.how.title": "Go nasıl çalışır?", - "go.how.body": - "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.", + "go.how.body": "Go ayda 10$'dır. OpenCode veya herhangi bir ajanla kullanabilirsiniz.", "go.how.step1.title": "Bir hesap oluşturun", "go.how.step1.beforeLink": "takip edin", "go.how.step1.link": "kurulum talimatları", "go.how.step2.title": "Go'ya abone olun", - "go.how.step2.link": "İlk ay $5", - "go.how.step2.afterLink": "sonrasında cömert limitlerle ayda 10$", + "go.how.step2.link": "Ayda 10$", + "go.how.step2.afterLink": "cömert limitlerle", "go.how.step3.title": "Kodlamaya başlayın", "go.how.step3.body": "açık kaynaklı modellere güvenilir erişimle", "go.privacy.title": "Gizliliğiniz bizim için önemlidir", @@ -332,11 +331,11 @@ export const dict = { "go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.", "go.faq.q3": "Go, Zen ile aynı mı?", "go.faq.a3": - "Hayır. Zen kullandıkça öde modelidir; Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar ve özenle seçilmiş model seçeneklerine cömert limitlerle güvenilir erişim sunar.", + "Hayır. Zen kullandıkça öde modelidir; Go ise ayda 10$'dır ve özenle seçilmiş model seçeneklerine cömert limitlerle güvenilir erişim sunar.", "go.faq.q4": "Go ne kadar?", "go.faq.a4.p1.beforePricing": "Go'nun maliyeti", - "go.faq.a4.p1.pricingLink": "İlk ay $5", - "go.faq.a4.p1.afterPricing": "sonrasında cömert limitlerle ayda 10$.", + "go.faq.a4.p1.pricingLink": "ayda 10$", + "go.faq.a4.p1.afterPricing": "cömert limitlerle.", "go.faq.a4.p2.beforeAccount": "Aboneliğinizi", "go.faq.a4.p2.accountLink": "hesabınızdan", "go.faq.a4.p3": "yönetebilirsiniz. İstediğiniz zaman iptal edin.", @@ -349,13 +348,16 @@ export const dict = { "go.faq.a5.used": "Kullanılır", "go.faq.a5.notUsed": "Kullanılmaz", "go.faq.a5.noAgreement": "Anlaşma yok", + "go.faq.a5.notZdr": "ZDR değil", "go.faq.a5.grokRetention": "ZDR, durum bilgisi tutan Responses API, Files and Collections ve Batch API dahil olmak üzere saklanan verilere bağlı önemli API özelliklerini devre dışı bırakır.", "go.faq.a5.gptRetention": "Tüm API özelliklerinin kullanımı için kötüye kullanım izleme günlükleri oluşturulur ve 30 güne kadar saklanır.", "go.faq.a5.learnMore": "Daha fazla bilgi", "go.faq.a5.deepseekRetention": - "ZDR anlaşması aylık olarak yenilenir. Mevcut anlaşma 31 Ağustos 2026 tarihine kadar geçerlidir.", + "ZDR anlaşması aylık olarak yenilenir. Mevcut anlaşma 30 Eylül 2026 tarihine kadar geçerlidir.", + "go.faq.a5.museRetention": + "İstemlerinizi ve tamamlamalarınızı gelecekteki Meta modellerini eğitmek için kullanma izni karşılığında büyük ölçüde indirimli token fiyatları.", "go.faq.a5.beforeExceptions": "Go modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu", @@ -370,7 +372,7 @@ export const dict = { "go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?", "go.faq.a9": - "Ücretsiz modeller, günlük 200 istek kotasıyla Big Pickle'ı ve o sırada mevcut olan promosyonel modelleri içerir. Go ise kayan zaman aralıklarında (5 saatlik, haftalık ve aylık) uygulanan daha yüksek istek kotalarıyla özenle seçilmiş model seçenekleri sunar. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", + "Ücretsiz modeller, günlük 200 istek kotasıyla Big Pickle'ı ve o sırada mevcut olan promosyonel modelleri içerir. Go ise kayan zaman aralıklarında (5 saatlik, haftalık ve aylık) uygulanan daha yüksek istek kotalarıyla özenle seçilmiş model seçenekleri sunar. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerindeki temel kullanım haklarına eşdeğerdir; modele özgü kullanım hakları farklılık gösterebilir (gerçek istek sayıları modele ve kullanıma göre değişir).", "zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.", "zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor", @@ -396,6 +398,9 @@ export const dict = { "zen.api.error.modelDisabled": "Model devre dışı", "zen.api.error.regionNotAllowed": "Bu modelin en son sürümü yalnızca Çin'de barındırılıyor ve kullanabilmek için açıkça onay vermeniz gerekiyor: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Bu model ülkenizde kullanılamıyor.", + "zen.api.error.trainingNotAllowed": + "Bu model, kalitesini artırmak için kullanılan verileri toplar. Modeli kullanabilmek için açıkça onay vermeniz gerekir: {{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} için ücretsiz promosyon sona erdi. OpenCode Go'ya abone olarak modeli kullanmaya devam edebilirsiniz - {{link}}", @@ -668,23 +673,32 @@ export const dict = { "workspace.lite.time.fewSeconds": "birkaç saniye", "workspace.lite.subscription.message": "OpenCode Go abonesisiniz.", "workspace.lite.subscription.manage": "Aboneliği Yönet", - "workspace.lite.subscription.rollingUsage": "Devam Eden Kullanım", + "workspace.lite.subscription.rollingUsage": "5 Saatlik Kullanım", + "workspace.lite.subscription.rollingQuota": "5 Saatlik Kota", "workspace.lite.subscription.weeklyUsage": "Haftalık Kullanım", + "workspace.lite.subscription.weeklyQuota": "Haftalık Kota", "workspace.lite.subscription.monthlyUsage": "Aylık Kullanım", + "workspace.lite.subscription.monthlyQuota": "Aylık Kota", "workspace.lite.subscription.resetsIn": "Sıfırlama süresi", + "workspace.lite.subscription.showDetails": "Ayrıntıları göster", + "workspace.lite.subscription.hideDetails": "Ayrıntıları gizle", + "workspace.lite.subscription.model": "Model", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Toplam", "workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın", "workspace.lite.subscription.selectProvider": 'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.', "workspace.lite.providers.title": "Sağlayıcılar", "workspace.lite.providers.description": "Yönlendirme için hangi sağlayıcıların kullanılacağını kontrol edin.", + "workspace.lite.providers.allowTraining": "İstek verileriyle eğitilen modellere izin ver", "workspace.lite.providers.useChina": "Çin'de barındırılan modelleri etkinleştir", "workspace.lite.black.message": "Şu anda OpenCode Black abonesisiniz veya bekleme listesindesiniz. Go'ya geçmek istiyorsanız lütfen önce aboneliğinizi iptal edin.", "workspace.lite.other.message": "Bu çalışma alanındaki başka bir üye zaten OpenCode Go abonesi. Çalışma alanı başına yalnızca bir üye abone olabilir.", "workspace.lite.promo.description": - "OpenCode Go {{price}} fiyatından başlar, sonrasında ayda 10$ olur ve cömert kullanım limitleriyle popüler açık kodlama modellerine güvenilir erişim sağlar.", - "workspace.lite.promo.price": "İlk ay $5", + "OpenCode Go {{price}} fiyatıyla cömert kullanım limitleri ve popüler açık kodlama modellerine güvenilir erişim sağlar.", + "workspace.lite.promo.price": "Ayda 10$", "workspace.lite.promo.modelsTitle": "Neler Dahil", "workspace.lite.promo.footer": "Plan öncelikle uluslararası kullanıcılar için tasarlanmıştır ve istikrarlı küresel erişim sağlar. Erken kullanım ve geri bildirimlerden öğrendiklerimiz doğrultusunda fiyatlandırma ve kullanım limitleri değişebilir.", diff --git a/packages/console/app/src/i18n/uk.ts b/packages/console/app/src/i18n/uk.ts index 6a4fbbd784..ac613a7d97 100644 --- a/packages/console/app/src/i18n/uk.ts +++ b/packages/console/app/src/i18n/uk.ts @@ -255,9 +255,9 @@ export const dict = { "zen.privacy.exceptionsLink": "такими винятками", "go.title": "OpenCode Go | Недорогі моделі кодування для всіх", - "go.banner.text": "DeepSeek V4 Flash отримує 2x ліміти використання протягом обмеженого часу", + "go.banner.text": "GLM-5.3-Flash отримує 2x ліміти використання протягом обмеженого часу", "go.meta.description": - "Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.", + "Go коштує $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.", "go.hero.title": "Недорогі моделі кодування для всіх", "go.hero.body": "Go надає агентне програмування програмістам у всьому світі, пропонуючи щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.", @@ -266,13 +266,13 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "Підписатися на Go", "go.cta.price": "$10/місяць", - "go.cta.promo": "$5 перший місяць", - "go.pricing.body": - "Використовуйте з будь-яким агентом. $5 перший місяць, потім $10/місяць. Поповнюйте за потреби. Скасуйте в будь-який час.", + "go.pricing.body": "Використовуйте з будь-яким агентом. $10/місяць. Поповнюйте за потреби. Скасуйте в будь-який час.", "go.graph.free": "Безкоштовно", "go.graph.freePill": "Big Pickle та безкоштовні моделі", "go.graph.go": "Go", "go.graph.label": "Запитів за 5 годин", + "go.graph.limitedRegions": "обмежені регіони", + "go.graph.limitedTime": "обмежений час", "go.graph.usageLimits": "Ліміти використання", "go.graph.aria": "Запитів за 5 год: {{free}} vs {{go}}", @@ -299,21 +299,20 @@ export const dict = { "go.testimonials.frank.quote": "Хотів би я досі бути в Nvidia.", "go.problem.title": "Яку проблему вирішує Go?", "go.problem.body": - "Ми зосереджені на тому, щоб зробити досвід OpenCode доступним для якомога більшої кількості людей. OpenCode Go — це недорога підписка: $5 за перший місяць, потім $10/місяць. Вона надає щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.", + "Ми зосереджені на тому, щоб зробити досвід OpenCode доступним для якомога більшої кількості людей. OpenCode Go — це недорога підписка за $10/місяць. Вона надає щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.", "go.problem.subtitle": " ", "go.problem.item1": "Недорога підписка", "go.problem.item2": "Щедрі ліміти та надійний доступ", "go.problem.item3": "Створено для якомога більшої кількості програмістів", "go.problem.item4": "Добірка моделей, протестованих для агентного кодування", "go.how.title": "Як працює Go", - "go.how.body": - "Go починається від $5 за перший місяць, потім $10/місяць. Використовуйте з OpenCode або будь-яким агентом.", + "go.how.body": "Go коштує $10/місяць. Використовуйте з OpenCode або будь-яким агентом.", "go.how.step1.title": "Створіть обліковий запис", "go.how.step1.beforeLink": "дотримуйтесь", "go.how.step1.link": "інструкцій з налаштування", "go.how.step2.title": "Підпишіться на Go", - "go.how.step2.link": "$5 перший місяць", - "go.how.step2.afterLink": "потім $10/місяць із щедрими лімітами", + "go.how.step2.link": "$10/місяць", + "go.how.step2.afterLink": "із щедрими лімітами", "go.how.step3.title": "Почніть кодувати", "go.how.step3.body": "з надійним доступом до моделей з відкритим кодом", "go.privacy.title": "Ваша конфіденційність важлива для нас", @@ -330,11 +329,11 @@ export const dict = { "go.faq.a2": "Go включає моделі, перелічені нижче, із щедрими лімітами та надійним доступом.", "go.faq.q3": "Чи Go те саме, що Zen?", "go.faq.a3": - "Ні. Zen — це плата за використання, тоді як Go починається від $5 за перший місяць, потім $10/місяць, із щедрими лімітами та надійним доступом до добірки моделей.", + "Ні. Zen — це плата за використання, тоді як Go коштує $10/місяць, із щедрими лімітами та надійним доступом до добірки моделей.", "go.faq.q4": "Скільки коштує Go?", "go.faq.a4.p1.beforePricing": "Go коштує", - "go.faq.a4.p1.pricingLink": "$5 за перший місяць", - "go.faq.a4.p1.afterPricing": "потім $10/місяць із щедрими лімітами.", + "go.faq.a4.p1.pricingLink": "$10/місяць", + "go.faq.a4.p1.afterPricing": "із щедрими лімітами.", "go.faq.a4.p2.beforeAccount": "Ви можете керувати підпискою в", "go.faq.a4.p2.accountLink": "обліковому записі", "go.faq.a4.p3": "Скасуйте в будь-який час.", @@ -347,12 +346,15 @@ export const dict = { "go.faq.a5.used": "Використовується", "go.faq.a5.notUsed": "Не використовується", "go.faq.a5.noAgreement": "Немає угоди", + "go.faq.a5.notZdr": "Не ZDR", "go.faq.a5.grokRetention": "ZDR вимикає важливі функції API, які залежать від збережених даних, зокрема Responses API зі збереженням стану, Files and Collections та Batch API.", "go.faq.a5.gptRetention": "Журнали моніторингу зловживань створюються для всіх випадків використання функцій API та зберігаються до 30 днів.", "go.faq.a5.learnMore": "Докладніше", - "go.faq.a5.deepseekRetention": "Угода ZDR поновлюється щомісяця. Поточна угода дійсна до 31 серпня 2026 року.", + "go.faq.a5.deepseekRetention": "Угода ZDR поновлюється щомісяця. Поточна угода дійсна до 30 вересня 2026 року.", + "go.faq.a5.museRetention": + "Значно знижені ціни на токени в обмін на дозвіл використовувати ваші промпти та відповіді для навчання майбутніх моделей Meta.", "go.faq.a5.beforeExceptions": "Моделі Go розміщені в США. Провайдери дотримуються політики нульового зберігання та не використовують ваші дані для навчання моделей, за", @@ -366,7 +368,7 @@ export const dict = { "go.faq.q9": "Яка різниця між безкоштовними моделями та Go?", "go.faq.a9": - "Безкоштовні моделі включають Big Pickle та доступні на той момент акційні моделі з квотою 200 запитів/день. Go пропонує добірку моделей із вищими квотами запитів, що застосовуються протягом ковзних періодів (5 годин, тижня та місяця), приблизно еквівалентними $12 за 5 годин, $30 на тиждень і $60 на місяць (фактична кількість запитів залежить від моделі та використання).", + "Безкоштовні моделі включають Big Pickle та доступні на той момент акційні моделі з квотою 200 запитів/день. Go пропонує добірку моделей із вищими квотами запитів, що застосовуються протягом ковзних періодів (5 годин, тижня та місяця), приблизно еквівалентними базовим лімітам $12 за 5 годин, $30 на тиждень і $60 на місяць; ліміти для окремих моделей можуть відрізнятися (фактична кількість запитів залежить від моделі та використання).", "zen.api.error.rateLimitExceeded": "Перевищено ліміт запитів. Спробуйте пізніше.", "zen.api.error.modelNotSupported": "Модель {{model}} не підтримується", @@ -392,6 +394,9 @@ export const dict = { "zen.api.error.modelDisabled": "Модель вимкнено", "zen.api.error.regionNotAllowed": "Остання версія цієї моделі доступна лише на серверах у Китаї, і для її використання потрібно надати явну згоду: {{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "Ця модель недоступна у вашій країні.", + "zen.api.error.trainingNotAllowed": + "Ця модель збирає дані, які використовуються для покращення її якості; для її використання потрібна ваша явна згода: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Безкоштовна акція для {{model}} закінчилася. Ви можете продовжити використання, підписавшись на OpenCode Go — {{link}}", @@ -664,20 +669,29 @@ export const dict = { "workspace.lite.time.fewSeconds": "кілька секунд", "workspace.lite.subscription.message": "Ви підписані на OpenCode Go.", "workspace.lite.subscription.manage": "Керувати підпискою", - "workspace.lite.subscription.rollingUsage": "Ковзне використання", + "workspace.lite.subscription.rollingUsage": "Використання за 5 годин", + "workspace.lite.subscription.rollingQuota": "Квота на 5 годин", "workspace.lite.subscription.weeklyUsage": "Тижневе використання", + "workspace.lite.subscription.weeklyQuota": "Тижнева квота", "workspace.lite.subscription.monthlyUsage": "Місячне використання", + "workspace.lite.subscription.monthlyQuota": "Місячна квота", "workspace.lite.subscription.resetsIn": "Скидається через", + "workspace.lite.subscription.showDetails": "Показати подробиці", + "workspace.lite.subscription.hideDetails": "Приховати подробиці", + "workspace.lite.subscription.model": "Модель", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "Усього", "workspace.lite.subscription.useBalance": "Використовуйте доступний баланс після досягнення лімітів", "workspace.lite.subscription.selectProvider": 'Виберіть "OpenCode Go" як провайдера в конфігурації opencode.', "workspace.lite.providers.title": "Провайдери", "workspace.lite.providers.description": "Керуйте провайдерами, які використовуються для маршрутизації.", + "workspace.lite.providers.allowTraining": "Дозволити моделі, що навчаються на даних запитів", "workspace.lite.providers.useChina": "Увімкнути моделі, розміщені в Китаї", "workspace.lite.black.message": "Ви вже підписані на OpenCode Black або в списку очікування. Спочатку скасуйте підписку, якщо хочете перейти на Go.", "workspace.lite.other.message": "Інший учасник цього робочого простору вже підписаний на OpenCode Go.", - "workspace.lite.promo.description": "OpenCode Go починається від {{price}}, потім $10/місяць, із щедрими лімітами.", - "workspace.lite.promo.price": "$5 за перший місяць", + "workspace.lite.promo.description": "OpenCode Go коштує {{price}} і має щедрі ліміти.", + "workspace.lite.promo.price": "$10/місяць", "workspace.lite.promo.modelsTitle": "Що включено", "workspace.lite.promo.footer": "План призначений насамперед для міжнародних користувачів і забезпечує стабільний глобальний доступ. Ціни та ліміти використання можуть змінюватися з урахуванням перших даних про використання та відгуків.", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index 22e71e1063..8be6f4b20e 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -244,8 +244,8 @@ export const dict = { "zen.privacy.exceptionsLink": "以下例外情况除外", "go.title": "OpenCode Go | 人人可用的低成本编程模型", - "go.banner.text": "DeepSeek V4 Flash 限时享受 2 倍使用额度", - "go.meta.description": "Go 首月 $5,之后 $10/月,提供充裕的使用限额,并可可靠访问领先的编程模型。", + "go.banner.text": "GLM-5.3-Flash 限时享受 2 倍使用额度", + "go.meta.description": "Go 每月 $10,提供充裕的使用限额,并可可靠访问领先的编程模型。", "go.hero.title": "人人可用的低成本编程模型", "go.hero.body": "Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。", @@ -254,12 +254,13 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "订阅 Go", "go.cta.price": "$10/月", - "go.cta.promo": "首月 $5", - "go.pricing.body": "可配合任何代理使用。首月 $5,之后 $10/月。如有需要可充值。随时取消。", + "go.pricing.body": "可配合任何代理使用。每月 $10。如有需要可充值。随时取消。", "go.graph.free": "免费", "go.graph.freePill": "Big Pickle 和免费模型", "go.graph.go": "Go", "go.graph.label": "每 5 小时请求数", + "go.graph.limitedRegions": "仅限部分地区", + "go.graph.limitedTime": "限时", "go.graph.usageLimits": "使用限制", "go.graph.aria": "每 5 小时请求数: {{free}} vs {{go}}", @@ -286,20 +287,20 @@ export const dict = { "go.testimonials.frank.quote": "我希望我还在 Nvidia。", "go.problem.title": "Go 解决了什么问题?", "go.problem.body": - "我们致力于将 OpenCode 体验带给尽可能多的人。OpenCode Go 是一款低成本订阅服务:首月 $5,之后 $10/月。它提供充裕的额度,并让您能可靠地使用最强大的开源模型。", + "我们致力于将 OpenCode 体验带给尽可能多的人。OpenCode Go 是一款每月 $10 的低成本订阅服务。它提供充裕的额度,并让您能可靠地使用最强大的开源模型。", "go.problem.subtitle": " ", "go.problem.item1": "低成本订阅定价", "go.problem.item2": "充裕的限额和可靠的访问", "go.problem.item3": "为尽可能多的程序员打造", "go.problem.item4": "经过代理编程测试的精选模型阵容", "go.how.title": "Go 如何工作", - "go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。", + "go.how.body": "Go 每月 $10。您可以将其与 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "创建账户", "go.how.step1.beforeLink": "遵循", "go.how.step1.link": "设置说明", "go.how.step2.title": "订阅 Go", - "go.how.step2.link": "首月 $5", - "go.how.step2.afterLink": "之后 $10/月,额度充裕", + "go.how.step2.link": "每月 $10", + "go.how.step2.afterLink": "额度充裕", "go.how.step3.title": "开始编程", "go.how.step3.body": "可靠访问开源模型", "go.privacy.title": "您的隐私对我们很重要", @@ -312,11 +313,11 @@ export const dict = { "go.faq.q2": "Go 包含哪些模型?", "go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。", "go.faq.q3": "Go 和 Zen 一样吗?", - "go.faq.a3": "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的限额,并可可靠访问精选模型阵容。", + "go.faq.a3": "不。Zen 是按量付费,而 Go 每月 $10,提供充裕的限额,并可可靠访问精选模型阵容。", "go.faq.q4": "Go 多少钱?", "go.faq.a4.p1.beforePricing": "Go 费用为", - "go.faq.a4.p1.pricingLink": "首月 $5", - "go.faq.a4.p1.afterPricing": "之后 $10/月,额度充裕。", + "go.faq.a4.p1.pricingLink": "每月 $10", + "go.faq.a4.p1.afterPricing": "额度充裕。", "go.faq.a4.p2.beforeAccount": "您可以在您的", "go.faq.a4.p2.accountLink": "账户", "go.faq.a4.p3": "中管理订阅。随时取消。", @@ -329,11 +330,13 @@ export const dict = { "go.faq.a5.used": "使用", "go.faq.a5.notUsed": "不使用", "go.faq.a5.noAgreement": "无协议", + "go.faq.a5.notZdr": "非 ZDR", "go.faq.a5.grokRetention": "ZDR 会禁用依赖所存储数据的重要 API 功能,包括有状态的 Responses API、Files and Collections 和 Batch API。", "go.faq.a5.gptRetention": "所有 API 功能的使用都会生成滥用监控日志,并最多保留 30 天。", + "go.faq.a5.museRetention": "以允许使用你的提示词和补全结果训练未来的 Meta 模型为交换,token 价格可获得大幅折扣。", "go.faq.a5.learnMore": "了解更多", - "go.faq.a5.deepseekRetention": "ZDR 协议每月续签。当前协议有效期至 2026 年 8 月 31 日。", + "go.faq.a5.deepseekRetention": "ZDR 协议每月续签。当前协议有效期至 2026 年 9 月 30 日。", "go.faq.a5.beforeExceptions": "Go 模型托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,", "go.faq.a5.exceptionsLink": "以下例外情况除外", "go.faq.q6": "我可以充值余额吗?", @@ -345,7 +348,7 @@ export const dict = { "go.faq.q9": "免费模型和 Go 之间的区别是什么?", "go.faq.a9": - "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 提供精选模型阵容,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", + "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 提供精选模型阵容,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60 的基础额度;具体额度可能因模型而异(实际请求计数因模型和使用情况而异)。", "zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。", "zen.api.error.modelNotSupported": "不支持模型 {{model}}", @@ -369,6 +372,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}", "zen.api.error.modelDisabled": "模型已禁用", "zen.api.error.regionNotAllowed": "该模型的最新版本仅在中国提供托管服务,需明确选择启用:{{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "该模型在你所在的国家或地区不可用。", + "zen.api.error.trainingNotAllowed": "该模型会收集用于提升其质量的数据,使用前需要你明确同意:{{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} 的限免活动已结束。您可以订阅 OpenCode Go 继续使用该模型 - {{link}}", "black.meta.title": "OpenCode Black | 访问全球顶尖编程模型", @@ -637,21 +642,30 @@ export const dict = { "workspace.lite.time.fewSeconds": "几秒钟", "workspace.lite.subscription.message": "您已订阅 OpenCode Go。", "workspace.lite.subscription.manage": "管理订阅", - "workspace.lite.subscription.rollingUsage": "滚动用量", + "workspace.lite.subscription.rollingUsage": "5 小时用量", + "workspace.lite.subscription.rollingQuota": "5 小时配额", "workspace.lite.subscription.weeklyUsage": "每周用量", + "workspace.lite.subscription.weeklyQuota": "每周配额", "workspace.lite.subscription.monthlyUsage": "每月用量", + "workspace.lite.subscription.monthlyQuota": "每月配额", "workspace.lite.subscription.resetsIn": "重置于", + "workspace.lite.subscription.showDetails": "显示详情", + "workspace.lite.subscription.hideDetails": "隐藏详情", + "workspace.lite.subscription.model": "模型", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "总计", "workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额", "workspace.lite.subscription.selectProvider": "在你的 opencode 配置中选择「OpenCode Go」作为提供商,即可使用 Go 模型。", "workspace.lite.providers.title": "提供商", "workspace.lite.providers.description": "控制用于路由的提供商。", + "workspace.lite.providers.allowTraining": "允许使用请求数据进行训练的模型", "workspace.lite.providers.useChina": "启用部署在中国的模型", "workspace.lite.black.message": "您当前已订阅 OpenCode Black 或在候补名单中。如需切换到 Go,请先取消订阅。", "workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Go。每个工作区只有一名成员可以订阅。", "workspace.lite.promo.description": - "OpenCode Go 起价为 {{price}},之后 $10/月,并提供对流行开放编码模型的可靠访问,同时享有充裕的使用限额。", - "workspace.lite.promo.price": "首月 $5", + "OpenCode Go 每月 {{price}},并提供对流行开放编码模型的可靠访问,同时享有充裕的使用限额。", + "workspace.lite.promo.price": "$10/月", "workspace.lite.promo.modelsTitle": "包含模型", "workspace.lite.promo.footer": "该计划主要面向国际用户,提供稳定的全球访问体验。随着我们持续了解早期使用情况并收集反馈,定价和使用限额可能会有所调整。", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 6818acf3fb..b1e7f3a3a5 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -244,8 +244,8 @@ export const dict = { "zen.privacy.exceptionsLink": "以下例外情況", "go.title": "OpenCode Go | 低成本全民編碼模型", - "go.banner.text": "DeepSeek V4 Flash 限時享有 2 倍使用額度", - "go.meta.description": "Go 首月 $5,之後 $10/月,提供充裕的使用限額,並可穩定存取領先的編碼模型。", + "go.banner.text": "GLM-5.3-Flash 限時享有 2 倍使用額度", + "go.meta.description": "Go 每月 $10,提供充裕的使用限額,並可穩定存取領先的編碼模型。", "go.hero.title": "低成本全民編碼模型", "go.hero.body": "Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。", @@ -254,12 +254,13 @@ export const dict = { "go.cta.template": "{{text}} {{price}}", "go.cta.text": "訂閱 Go", "go.cta.price": "$10/月", - "go.cta.promo": "首月 $5", - "go.pricing.body": "可搭配任何代理使用。首月 $5,之後 $10/月。如有需要可儲值。隨時取消。", + "go.pricing.body": "可搭配任何代理使用。每月 $10。如有需要可儲值。隨時取消。", "go.graph.free": "免費", "go.graph.freePill": "Big Pickle 與免費模型", "go.graph.go": "Go", "go.graph.label": "每 5 小時請求數", + "go.graph.limitedRegions": "僅限部分地區", + "go.graph.limitedTime": "限時", "go.graph.usageLimits": "使用限制", "go.graph.aria": "每 5 小時請求數:{{free}} vs {{go}}", @@ -286,20 +287,20 @@ export const dict = { "go.testimonials.frank.quote": "我希望我還在 Nvidia。", "go.problem.title": "Go 正在解決什麼問題?", "go.problem.body": - "我們致力於將 OpenCode 體驗帶給盡可能多的人。OpenCode Go 是一款低成本訂閱服務:首月 $5,之後 $10/月。它提供充裕的額度,並讓您能可靠地使用最強大的開源模型。", + "我們致力於將 OpenCode 體驗帶給盡可能多的人。OpenCode Go 是一款每月 $10 的低成本訂閱服務。它提供充裕的額度,並讓您能可靠地使用最強大的開源模型。", "go.problem.subtitle": " ", "go.problem.item1": "低成本訂閱定價", "go.problem.item2": "寬裕的限額與穩定存取", "go.problem.item3": "專為盡可能多的程式設計師打造", "go.problem.item4": "針對代理編碼測試的精選模型陣容", "go.how.title": "Go 如何運作", - "go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。", + "go.how.body": "Go 每月 $10。您可以將其與 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "建立帳號", "go.how.step1.beforeLink": "遵循", "go.how.step1.link": "設定說明", "go.how.step2.title": "訂閱 Go", - "go.how.step2.link": "首月 $5", - "go.how.step2.afterLink": "之後 $10/月,額度充裕", + "go.how.step2.link": "每月 $10", + "go.how.step2.afterLink": "額度充裕", "go.how.step3.title": "開始編碼", "go.how.step3.body": "穩定存取開源模型", "go.privacy.title": "你的隱私對我們很重要", @@ -312,11 +313,11 @@ export const dict = { "go.faq.q2": "Go 包含哪些模型?", "go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。", "go.faq.q3": "Go 與 Zen 一樣嗎?", - "go.faq.a3": "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的限額,並可穩定存取精選模型陣容。", + "go.faq.a3": "不。Zen 是按量付費,而 Go 每月 $10,提供充裕的限額,並可穩定存取精選模型陣容。", "go.faq.q4": "Go 費用是多少?", "go.faq.a4.p1.beforePricing": "Go 費用為", - "go.faq.a4.p1.pricingLink": "首月 $5", - "go.faq.a4.p1.afterPricing": "之後 $10/月,額度充裕。", + "go.faq.a4.p1.pricingLink": "每月 $10", + "go.faq.a4.p1.afterPricing": "額度充裕。", "go.faq.a4.p2.beforeAccount": "你可以在你的", "go.faq.a4.p2.accountLink": "帳戶", "go.faq.a4.p3": "中管理訂閱。隨時取消。", @@ -329,11 +330,13 @@ export const dict = { "go.faq.a5.used": "使用", "go.faq.a5.notUsed": "不使用", "go.faq.a5.noAgreement": "無協議", + "go.faq.a5.notZdr": "非 ZDR", "go.faq.a5.grokRetention": "ZDR 會停用依賴儲存資料的重要 API 功能,包括具狀態的 Responses API、Files and Collections 與 Batch API。", "go.faq.a5.gptRetention": "所有 API 功能的使用都會產生濫用監控日誌,並保留最多 30 天。", + "go.faq.a5.museRetention": "以允許使用您的提示詞和生成結果來訓練未來的 Meta 模型為交換,token 價格可享大幅折扣。", "go.faq.a5.learnMore": "了解更多", - "go.faq.a5.deepseekRetention": "ZDR 協議每月續簽。目前的協議有效至 2026 年 8 月 31 日。", + "go.faq.a5.deepseekRetention": "ZDR 協議每月續簽。目前的協議有效至 2026 年 9 月 30 日。", "go.faq.a5.beforeExceptions": "Go 模型託管在美國。供應商遵循零留存政策,不會將你的資料用於模型訓練,但有", "go.faq.a5.exceptionsLink": "以下例外", "go.faq.q6": "我可以儲值額度嗎?", @@ -345,7 +348,7 @@ export const dict = { "go.faq.q9": "免費模型與 Go 有什麼區別?", "go.faq.a9": - "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 提供精選模型陣容,並在滾動視窗(5 小時、每週和每月)內提供更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", + "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 提供精選模型陣容,並在滾動視窗(5 小時、每週和每月)內提供更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60 的基礎額度;具體額度可能因模型而異(實際請求數因模型和使用情況而異)。", "zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。", "zen.api.error.modelNotSupported": "不支援模型 {{model}}", @@ -369,6 +372,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}", "zen.api.error.modelDisabled": "模型已停用", "zen.api.error.regionNotAllowed": "此模型的最新版本僅於中國託管,且需明確選擇啟用:{{consoleGoUrl}}", + "zen.api.error.countryNotAllowed": "此模型無法在你所在的國家或地區使用。", + "zen.api.error.trainingNotAllowed": "此模型會收集用於提升其品質的資料,使用前須經你明確同意:{{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} 的限免活动已結束。您可以訂閱 OpenCode Go 繼續使用該模型 - {{link}}", "black.meta.title": "OpenCode Black | 存取全球最佳編碼模型", @@ -637,21 +642,30 @@ export const dict = { "workspace.lite.time.fewSeconds": "幾秒", "workspace.lite.subscription.message": "您已訂閱 OpenCode Go。", "workspace.lite.subscription.manage": "管理訂閱", - "workspace.lite.subscription.rollingUsage": "滾動使用量", + "workspace.lite.subscription.rollingUsage": "5 小時使用量", + "workspace.lite.subscription.rollingQuota": "5 小時配額", "workspace.lite.subscription.weeklyUsage": "每週使用量", + "workspace.lite.subscription.weeklyQuota": "每週配額", "workspace.lite.subscription.monthlyUsage": "每月使用量", + "workspace.lite.subscription.monthlyQuota": "每月配額", "workspace.lite.subscription.resetsIn": "重置時間:", + "workspace.lite.subscription.showDetails": "顯示詳情", + "workspace.lite.subscription.hideDetails": "隱藏詳情", + "workspace.lite.subscription.model": "模型", + "workspace.lite.subscription.contribution": "%", + "workspace.lite.subscription.total": "總計", "workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額", "workspace.lite.subscription.selectProvider": "在您的 opencode 設定中選擇「OpenCode Go」作為提供商,即可使用 Go 模型。", "workspace.lite.providers.title": "提供商", "workspace.lite.providers.description": "控制用於路由的提供商。", + "workspace.lite.providers.allowTraining": "允許使用請求資料進行訓練的模型", "workspace.lite.providers.useChina": "啟用部署在中國的模型", "workspace.lite.black.message": "您目前已訂閱 OpenCode Black 或在候補名單中。若要切換至 Go,請先取消訂閱。", "workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Go。每個工作區只能有一位成員訂閱。", "workspace.lite.promo.description": - "OpenCode Go 起價為 {{price}},之後 $10/月,並提供對熱門開放編碼模型的可靠存取,同時享有充裕的使用額度。", - "workspace.lite.promo.price": "首月 $5", + "OpenCode Go 每月 {{price}},並提供對熱門開放編碼模型的可靠存取,同時享有充裕的使用額度。", + "workspace.lite.promo.price": "$10/月", "workspace.lite.promo.modelsTitle": "包含模型", "workspace.lite.promo.footer": "此方案主要為國際使用者設計,提供穩定的全球存取服務。隨著我們從初期使用情況和回饋中持續了解需求,價格和使用額度可能會有所調整。", diff --git a/packages/console/app/src/lib/inference-proxy.ts b/packages/console/app/src/lib/inference-proxy.ts new file mode 100644 index 0000000000..7de73a961a --- /dev/null +++ b/packages/console/app/src/lib/inference-proxy.ts @@ -0,0 +1,92 @@ +import { Resource } from "@opencode-ai/console-resource" +import { and, Database, eq, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js" +import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" +import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js" +import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" + +const paths: Record = { + "POST /zen/v1/chat/completions": "/openai/v1/chat/completions", + "POST /zen/v1/responses": "/openai/v1/responses", + "POST /zen/v1/messages": "/anthropic/v1/messages", +} + +export async function proxyInference( + request: Request, + generation: { + provider?: "openai" | "anthropic" | "google" + /** The provider's native model ID, not the public Zen alias. */ + model?: string + body: (model?: string) => ReadableStream + }, +): Promise { + const url = new URL(request.url) + const path = + paths[`${request.method} ${url.pathname}`] ?? + (request.method === "POST" && + /^\/zen\/v1\/models\/[^/]+:(?:generateContent|streamGenerateContent)$/.test(url.pathname) + ? url.pathname.replace("/zen/v1/models/", "/google/v1beta/models/") + : undefined) + if (!path) return undefined + + const key = path.startsWith("/anthropic/") + ? request.headers.get("x-api-key") + : path.startsWith("/google/") + ? request.headers.get("x-goog-api-key") + : request.headers.get("authorization")?.split(" ")[1] + if (!key || key === "public") return undefined + + // Routing only; the destination owns authentication and revocation after cutover. + const workspace = await Database.use((tx) => + tx + .select({ + id: WorkspaceTable.id, + migratedAt: WorkspaceTable.migrated_at, + provider: ProviderTable.provider, + }) + .from(KeyTable) + .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID)) + .leftJoin( + ProviderTable, + generation.provider + ? and( + eq(ProviderTable.workspaceID, KeyTable.workspaceID), + eq(ProviderTable.provider, generation.provider), + isNull(ProviderTable.timeDeleted), + sql`length(${ProviderTable.credentials}) > 0`, + ) + : sql`false`, + ) + .where(eq(KeyTable.key, key)) + .limit(1) + .then((rows) => rows[0]), + ) + if (!workspace?.migratedAt) return undefined + const model = workspace.provider ? generation.model : undefined + if (workspace.provider && !model) throw new Error("Legacy BYOK model mapping is unavailable") + + const destination = new URL(Resource.ConsoleMigration.inferenceUrl) + // Imported connections must use this same workspace/provider-derived ID. + const target = model + ? `/custom/conn_${workspace.id.slice(4)}_${workspace.provider}${ + path.startsWith("/google/") + ? `/models/${encodeURIComponent(model)}${url.pathname.slice(url.pathname.lastIndexOf(":"))}` + : url.pathname.slice("/zen/v1".length) + }` + : path + destination.pathname = `${destination.pathname.replace(/\/$/, "")}${target}` + destination.search = url.search + destination.hash = "" + + // Model extraction has already read part of the body; forward its replay stream. + const forwarded = new Request( + destination, + new Request(request, { method: request.method, body: generation.body(model) }), + ) + forwarded.headers.set("authorization", `Bearer ${key}`) + const ip = request.headers.get("cf-connecting-ip") + if (ip) forwarded.headers.set("x-real-ip", ip) + const requestID = request.headers.get("x-opencode-request-id") ?? request.headers.get("x-opencode-request") + if (requestID) forwarded.headers.set("x-opencode-request-id", requestID) + + return fetch(forwarded, { redirect: "manual" }) +} diff --git a/packages/console/app/src/lib/lite-usage.ts b/packages/console/app/src/lib/lite-usage.ts new file mode 100644 index 0000000000..618c89e92d --- /dev/null +++ b/packages/console/app/src/lib/lite-usage.ts @@ -0,0 +1,73 @@ +export type LiteUsageBreakdownSource = { + model: string + name: string + cost: number + quotaCost: number + multiplier?: number + estimated: boolean +} + +export type LiteUsageBreakdownItem = { + model: string + name: string + cost?: number + multiplier?: number + quotaCost: number + contributionPercent: number + estimated: boolean +} + +export function buildLiteUsageBreakdown(input: { usage: number; limit: number; sources: LiteUsageBreakdownSource[] }) { + // Legacy usage can resolve to the same rate as a separately grouped recorded multiplier. + const groups = new Map() + input.sources.forEach((item) => { + const key = JSON.stringify([item.model, item.multiplier]) + const row = groups.get(key) + if (!row) { + groups.set(key, { ...item }) + return + } + row.cost += item.cost + row.quotaCost += item.quotaCost + row.estimated ||= item.estimated + }) + const rows: LiteUsageBreakdownItem[] = Array.from(groups.values()) + .filter((item) => item.cost !== 0 || item.quotaCost !== 0) + .sort((a, b) => b.quotaCost - a.quotaCost) + .map((item) => ({ + ...item, + contributionPercent: 0, + })) + + const usagePercent = getUsagePercent(input.usage, input.limit) + const target = Math.max(0, Math.round(usagePercent * 10)) + const totalQuota = rows.reduce((total, item) => total + Math.max(0, item.quotaCost), 0) + const units = rows.map((item) => { + const exact = totalQuota === 0 ? 0 : (Math.max(0, item.quotaCost) / totalQuota) * target + const value = Math.floor(exact) + return { item, exact, value } + }) + const remaining = target - units.reduce((total, item) => total + item.value, 0) + const ranked = units.toSorted((a, b) => b.exact - b.value - (a.exact - a.value)) + Array.from({ length: ranked.length === 0 ? 0 : remaining }).forEach((_, index) => { + ranked[index % ranked.length].value += 1 + }) + units.forEach((unit) => (unit.item.contributionPercent = unit.value / 10)) + + return { + usage: input.usage, + limit: input.limit, + usagePercent, + rows, + } +} + +export function getModelQuotaLimit(limit: number, multiplier?: number) { + if (multiplier === undefined || multiplier <= 0) return + return limit / multiplier +} + +export function getUsagePercent(amount: number, limit: number) { + if (limit === 0) return 0 + return Math.round((amount / limit) * 1000) / 10 +} diff --git a/packages/console/app/src/lib/request-country.ts b/packages/console/app/src/lib/request-country.ts index a4e39eb607..5806fc2f64 100644 --- a/packages/console/app/src/lib/request-country.ts +++ b/packages/console/app/src/lib/request-country.ts @@ -1,5 +1,43 @@ +const MUSE_SPARK_BLOCKED_COUNTRIES = new Set([ + "AF", + "BY", + "CN", + "CU", + "EH", + "ER", + "ET", + "HK", + "HT", + "IQ", + "IR", + "KH", + "KP", + "LY", + "MM", + "MO", + "NI", + "PK", + "RU", + "SO", + "SY", + "VE", +]) + export function countryFromRequest(request: Request | undefined) { if (!request) return undefined const cloudflareRequest = request as Request & { cf?: { country?: string } } return cloudflareRequest.cf?.country ?? request.headers.get("cf-ipcountry") ?? undefined } + +export function isModelCountryRestricted(model: string, country: string | undefined) { + return ( + [ + "muse-spark-1.3-contributor", + "muse-spark-1.3-contributor-free", + "muse-spark-1.2-contributor", + "muse-spark-1.2-contributor-free", + ].includes(model) && + country !== undefined && + MUSE_SPARK_BLOCKED_COUNTRIES.has(country.toUpperCase()) + ) +} diff --git a/packages/console/app/src/lib/server-action.ts b/packages/console/app/src/lib/server-action.ts new file mode 100644 index 0000000000..1d82b56978 --- /dev/null +++ b/packages/console/app/src/lib/server-action.ts @@ -0,0 +1,11 @@ +export function sanitizeServerActionRequest(request: Request) { + const requestUrl = new URL(request.url) + if (requestUrl.pathname !== "/_server") return request + + const referer = request.headers.get("referer") + if (referer && URL.canParse(referer) && new URL(referer).origin === requestUrl.origin) return request + + const sanitized = new Request(request) + sanitized.headers.set("referer", requestUrl.origin) + return sanitized +} diff --git a/packages/console/app/src/lib/stats-proxy.ts b/packages/console/app/src/lib/stats-proxy.ts index 48e95bd74a..b8db97c063 100644 --- a/packages/console/app/src/lib/stats-proxy.ts +++ b/packages/console/app/src/lib/stats-proxy.ts @@ -1,8 +1,9 @@ import type { APIEvent } from "@solidjs/start/server" -import { Resource } from "@opencode-ai/console-resource" +import { Resource, waitUntil } from "@opencode-ai/console-resource" import { LOCALE_HEADER, cookie, localeFromRequest, route, tag } from "~/lib/language" const dataPath = "/data" +const statsCacheParam = "__opencode_stats_locale" export async function statsProxy(evt: APIEvent) { const req = evt.request.clone() @@ -10,6 +11,13 @@ export async function statsProxy(evt: APIEvent) { const redirect = redirectToLocalizedData(req, new URL(req.url), locale) if (redirect) return redirect + const cache = defaultCache(caches) + const cacheKey = statsCacheKey(req, locale) + if (cacheKey) { + const cached = await cache.match(cacheKey) + if (cached) return withStatsCacheStatus(cached, "HIT") + } + const targetUrl = new URL(req.url) targetUrl.protocol = "https:" targetUrl.hostname = Resource.App.stage === "production" ? "stats.opencode.ai" : "stats.dev.opencode.ai" @@ -40,13 +48,18 @@ export async function statsProxy(evt: APIEvent) { headers.delete("content-encoding") headers.delete("content-length") headers.delete("etag") + headers.delete("set-cookie") appendVary(headers, "Accept-Language", "Cookie", LOCALE_HEADER) - return new Response(rewriteStatsHtml(await response.text()), { + const result = new Response(rewriteStatsHtml(await response.text()), { status: response.status, statusText: response.statusText, headers, }) + if (!cacheKey || !response.ok) return result + + void waitUntil(cache.put(cacheKey, result.clone())) + return withStatsCacheStatus(result, "MISS") } export function statsRedirect(evt: APIEvent) { @@ -64,6 +77,34 @@ function rewriteStatsHtml(html: string) { return html.replaceAll('"/_build/', `"${dataPath}/_build/`).replaceAll("'/_build/", `'${dataPath}/_build/`) } +function statsCacheKey(request: Request, locale: ReturnType): Request | undefined { + if (request.method !== "GET") return undefined + if (!acceptsHtml(request)) return undefined + if (isDataBypassPath(new URL(request.url).pathname)) return undefined + + const url = new URL(request.url) + const additionalModels = url.searchParams.get("add") + url.search = "" + if (additionalModels) url.searchParams.set("add", additionalModels) + url.searchParams.set(statsCacheParam, locale) + return new Request(url) +} + +function defaultCache(storage: CacheStorage) { + if (!isCloudflareCacheStorage(storage)) throw new Error("Cloudflare default cache is unavailable") + return storage.default +} + +function isCloudflareCacheStorage(storage: CacheStorage): storage is CacheStorage & { default: Cache } { + return "default" in storage +} + +function withStatsCacheStatus(response: Response, status: "HIT" | "MISS") { + const headers = new Headers(response.headers) + headers.set("x-opencode-stats-cache", status) + return new Response(response.body, { status: response.status, statusText: response.statusText, headers }) +} + function redirectToLocalizedData(request: Request, url: URL, locale: ReturnType) { if (locale === "en") return null if (request.headers.get(LOCALE_HEADER)) return null diff --git a/packages/console/app/src/middleware.ts b/packages/console/app/src/middleware.ts index ad5aa09e2a..614cc87bcf 100644 --- a/packages/console/app/src/middleware.ts +++ b/packages/console/app/src/middleware.ts @@ -1,9 +1,12 @@ import { createMiddleware } from "@solidjs/start/middleware" import { LOCALE_HEADER, cookie, fromPathname, strip } from "~/lib/language" import { normalizeReferralCode, referralCookie } from "~/lib/referral-invite" +import { sanitizeServerActionRequest } from "~/lib/server-action" export default createMiddleware({ - onRequest(event) { + async onRequest(event) { + event.request = sanitizeServerActionRequest(event.request) + const url = new URL(event.request.url) const locale = fromPathname(url.pathname) if (locale) { diff --git a/packages/console/app/src/routes/api/support/actions/reset-quota.ts b/packages/console/app/src/routes/api/support/actions/reset-quota.ts new file mode 100644 index 0000000000..6f5c2812cf --- /dev/null +++ b/packages/console/app/src/routes/api/support/actions/reset-quota.ts @@ -0,0 +1,24 @@ +import type { APIEvent } from "@solidjs/start/server" +import { Quota } from "@opencode-ai/console-core/quota.js" +import { safeEqual } from "@opencode-ai/console-core/util/crypto.js" +import { Resource } from "@opencode-ai/console-resource" +import z from "zod" + +const Body = z.object({ + workspaceID: z.string().startsWith("wrk_"), + plan: z.enum(["lite", "subscription"]), +}) + +export async function POST(event: APIEvent) { + if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) { + return Response.json({ error: "Unauthorized" }, { status: 401 }) + } + + const body = Body.safeParse(await event.request.json().catch(() => undefined)) + if (!body.success) { + return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 }) + } + return Quota.reset(body.data) + .then((result) => Response.json({ success: true, message: "Usage counters reset to zero", result })) + .catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 })) +} diff --git a/packages/console/app/src/routes/api/support/actions/unblock-workspace.ts b/packages/console/app/src/routes/api/support/actions/unblock-workspace.ts new file mode 100644 index 0000000000..fd4baa07fd --- /dev/null +++ b/packages/console/app/src/routes/api/support/actions/unblock-workspace.ts @@ -0,0 +1,21 @@ +import type { APIEvent } from "@solidjs/start/server" +import { Workspace } from "@opencode-ai/console-core/workspace.js" +import { safeEqual } from "@opencode-ai/console-core/util/crypto.js" +import { Resource } from "@opencode-ai/console-resource" +import z from "zod" + +const Body = z.object({ workspaceID: z.string().startsWith("wrk_") }) + +export async function POST(event: APIEvent) { + if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) { + return Response.json({ error: "Unauthorized" }, { status: 401 }) + } + + const body = Body.safeParse(await event.request.json().catch(() => undefined)) + if (!body.success) { + return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 }) + } + return Workspace.unblock(body.data) + .then(() => Response.json({ success: true, message: "Workspace unblocked" })) + .catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 })) +} diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index 8bce3cd464..b8f01842d0 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -103,9 +103,6 @@ export default function Black() { - - {i18n.t("black.action.continue")} - diff --git a/packages/console/app/src/routes/black/subscribe/[plan].tsx b/packages/console/app/src/routes/black/subscribe/[plan].tsx deleted file mode 100644 index c29c5fac80..0000000000 --- a/packages/console/app/src/routes/black/subscribe/[plan].tsx +++ /dev/null @@ -1,489 +0,0 @@ -import { A, createAsync, query, redirect, useParams } from "@solidjs/router" -import { Title } from "@solidjs/meta" -import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js" -import { type Stripe, type PaymentMethod, loadStripe } from "@stripe/stripe-js" -import { Elements, PaymentElement, useStripe, useElements, AddressElement } from "solid-stripe" -import { PlanID, plans } from "../common" -import { getActor, useAuthSession } from "~/context/auth" -import { withActor } from "~/context/auth.withActor" -import { Actor } from "@opencode-ai/console-core/actor.js" -import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" -import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" -import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" -import { createList } from "solid-list" -import { Modal } from "~/component/modal" -import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js" -import { Billing } from "@opencode-ai/console-core/billing.js" -import { useI18n } from "~/context/i18n" -import { useLanguage } from "~/context/language" -import { formError } from "~/lib/form-error" -import { Resource } from "@opencode-ai/console-resource" - -const getEnabled = query(async () => { - "use server" - return Resource.App.stage !== "production" -}, "black.subscribe.enabled") - -const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record -const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!) - -const getWorkspaces = query(async (plan: string) => { - "use server" - const actor = await getActor() - if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe/" + plan) - return withActor(async () => { - return Database.use((tx) => - tx - .select({ - id: WorkspaceTable.id, - name: WorkspaceTable.name, - slug: WorkspaceTable.slug, - billing: { - customerID: BillingTable.customerID, - paymentMethodID: BillingTable.paymentMethodID, - paymentMethodType: BillingTable.paymentMethodType, - paymentMethodLast4: BillingTable.paymentMethodLast4, - subscriptionID: BillingTable.subscriptionID, - timeSubscriptionBooked: BillingTable.timeSubscriptionBooked, - }, - }) - .from(UserTable) - .innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id)) - .innerJoin(BillingTable, eq(WorkspaceTable.id, BillingTable.workspaceID)) - .where( - and( - eq(UserTable.accountID, Actor.account()), - isNull(WorkspaceTable.timeDeleted), - isNull(UserTable.timeDeleted), - ), - ), - ) - }) -}, "black.subscribe.workspaces") - -const createSetupIntent = async (input: { plan: string; workspaceID: string }) => { - "use server" - const { plan, workspaceID } = input - - if (!plan || !["20", "100", "200"].includes(plan)) return { error: formError.invalidPlan } - if (!workspaceID) return { error: formError.workspaceRequired } - - return withActor(async () => { - const session = await useAuthSession() - const account = session.data.account?.[session.data.current ?? ""] - const email = account?.email - - const customer = await Database.use((tx) => - tx - .select({ - customerID: BillingTable.customerID, - subscriptionID: BillingTable.subscriptionID, - }) - .from(BillingTable) - .where(eq(BillingTable.workspaceID, workspaceID)) - .then((rows) => rows[0]), - ) - if (customer?.subscriptionID) { - return { error: formError.alreadySubscribed } - } - - let customerID = customer?.customerID - if (!customerID) { - const customer = await Billing.stripe().customers.create({ - email, - metadata: { - workspaceID, - }, - }) - customerID = customer.id - await Database.use((tx) => - tx - .update(BillingTable) - .set({ - customerID, - }) - .where(eq(BillingTable.workspaceID, workspaceID)), - ) - } - - const intent = await Billing.stripe().setupIntents.create({ - customer: customerID, - payment_method_types: ["card"], - metadata: { - workspaceID, - }, - }) - - return { clientSecret: intent.client_secret ?? undefined } - }, workspaceID) -} - -const bookSubscription = async (input: { - workspaceID: string - plan: PlanID - paymentMethodID: string - paymentMethodType: string - paymentMethodLast4?: string -}) => { - "use server" - return withActor( - () => - Database.use((tx) => - tx - .update(BillingTable) - .set({ - paymentMethodID: input.paymentMethodID, - paymentMethodType: input.paymentMethodType, - paymentMethodLast4: input.paymentMethodLast4, - subscriptionPlan: input.plan, - timeSubscriptionBooked: new Date(), - }) - .where(eq(BillingTable.workspaceID, input.workspaceID)), - ), - input.workspaceID, - ) -} - -interface SuccessData { - plan: string - paymentMethodType: string - paymentMethodLast4?: string -} - -function Failure(props: { message: string }) { - const i18n = useI18n() - - return ( -
-

- {i18n.t("black.subscribe.failurePrefix")} {props.message} -

-
- ) -} - -function Success(props: SuccessData) { - const i18n = useI18n() - - return ( -
-

{i18n.t("black.subscribe.success.title")}

-
-
-
{i18n.t("black.subscribe.success.subscriptionPlan")}
-
{i18n.t("black.subscribe.success.planName", { plan: props.plan })}
-
-
-
{i18n.t("black.subscribe.success.amount")}
-
{i18n.t("black.subscribe.success.amountValue", { plan: props.plan })}
-
-
-
{i18n.t("black.subscribe.success.paymentMethod")}
-
- {props.paymentMethodType}}> - - {props.paymentMethodType} - {props.paymentMethodLast4} - - -
-
-
-
{i18n.t("black.subscribe.success.dateJoined")}
-
{new Date().toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })}
-
-
-

{i18n.t("black.subscribe.success.chargeNotice")}

-
- ) -} - -function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data: SuccessData) => void }) { - const i18n = useI18n() - const stripe = useStripe() - const elements = useElements() - const [error, setError] = createSignal(undefined) - const [loading, setLoading] = createSignal(false) - - const handleSubmit = async (e: Event) => { - e.preventDefault() - if (!stripe() || !elements()) return - - setLoading(true) - setError(undefined) - - const result = await elements()!.submit() - if (result.error) { - setError(result.error.message ?? i18n.t("black.subscribe.error.generic")) - setLoading(false) - return - } - - const { error: confirmError, setupIntent } = await stripe()!.confirmSetup({ - elements: elements()!, - confirmParams: { - expand: ["payment_method"], - payment_method_data: { - allow_redisplay: "always", - }, - }, - redirect: "if_required", - }) - - if (confirmError) { - setError(confirmError.message ?? i18n.t("black.subscribe.error.generic")) - setLoading(false) - return - } - - if (setupIntent?.status === "succeeded") { - const pm = setupIntent.payment_method as PaymentMethod - - await bookSubscription({ - workspaceID: props.workspaceID, - plan: props.plan, - paymentMethodID: pm.id, - paymentMethodType: pm.type, - paymentMethodLast4: pm.card?.last4, - }) - - props.onSuccess({ - plan: props.plan, - paymentMethodType: pm.type, - paymentMethodLast4: pm.card?.last4, - }) - } - - setLoading(false) - } - - return ( -
- - - -

{error()}

-
- -

{i18n.t("black.subscribe.form.chargeNotice")}

- - ) -} - -export default function BlackSubscribe() { - const params = useParams() - const i18n = useI18n() - const language = useLanguage() - const enabled = createAsync(() => getEnabled()) - const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"] - const plan = planData.id - - const workspaces = createAsync(() => getWorkspaces(plan)) - const [selectedWorkspace, setSelectedWorkspace] = createSignal(undefined) - const [success, setSuccess] = createSignal(undefined) - const [failure, setFailure] = createSignal(undefined) - const [clientSecret, setClientSecret] = createSignal(undefined) - const [stripe, setStripe] = createSignal(undefined) - - const formatError = (error: string) => { - if (error === formError.invalidPlan) return i18n.t("black.subscribe.error.invalidPlan") - if (error === formError.workspaceRequired) return i18n.t("black.subscribe.error.workspaceRequired") - if (error === formError.alreadySubscribed) return i18n.t("black.subscribe.error.alreadySubscribed") - if (error === "Invalid plan") return i18n.t("black.subscribe.error.invalidPlan") - if (error === "Workspace ID is required") return i18n.t("black.subscribe.error.workspaceRequired") - if (error === "This workspace already has a subscription") return i18n.t("black.subscribe.error.alreadySubscribed") - return error - } - - // Resolve stripe promise once - createEffect(() => { - void stripePromise.then((s) => { - if (s) setStripe(s) - }) - }) - - // Auto-select if only one workspace - createEffect(() => { - const ws = workspaces() - if (ws?.length === 1 && !selectedWorkspace()) { - setSelectedWorkspace(ws[0].id) - } - }) - - // Fetch setup intent when workspace is selected (unless workspace already has payment method) - createEffect(async () => { - const id = selectedWorkspace() - if (!id) return - - const ws = workspaces()?.find((w) => w.id === id) - if (ws?.billing?.subscriptionID) { - setFailure(i18n.t("black.subscribe.error.alreadySubscribed")) - return - } - if (ws?.billing?.paymentMethodID) { - if (!ws?.billing?.timeSubscriptionBooked) { - await bookSubscription({ - workspaceID: id, - plan: planData.id, - paymentMethodID: ws.billing.paymentMethodID!, - paymentMethodType: ws.billing.paymentMethodType!, - paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined, - }) - } - setSuccess({ - plan: planData.id, - paymentMethodType: ws.billing.paymentMethodType!, - paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined, - }) - return - } - - const result = await createSetupIntent({ plan, workspaceID: id }) - if (result.error) { - setFailure(formatError(result.error)) - } else if ("clientSecret" in result) { - setClientSecret(result.clientSecret) - } - }) - - // Keyboard navigation for workspace picker - const { active, setActive, onKeyDown } = createList({ - items: () => workspaces()?.map((w) => w.id) ?? [], - initialActive: null, - }) - - const handleSelectWorkspace = (id: string) => { - setSelectedWorkspace(id) - } - - let listRef: HTMLUListElement | undefined - - // Show workspace picker if multiple workspaces and none selected - const showWorkspacePicker = () => { - const ws = workspaces() - return ws && ws.length > 1 && !selectedWorkspace() - } - - return ( - - {i18n.t("black.subscribe.title")} -
-
- - {(data) => } - {(data) => } - - <> -
-

{i18n.t("black.subscribe.title")}

-

- ${planData.id}{" "} - {i18n.t("black.price.perMonth")} - - {(multiplier) => {i18n.t(multiplier())}} - -

-
-
-

{i18n.t("black.subscribe.paymentMethod")}

- - -

- {selectedWorkspace() - ? i18n.t("black.subscribe.loadingPaymentForm") - : i18n.t("black.subscribe.selectWorkspaceToContinue")} -

-
- } - > - - - - - -
-
-
- - {/* Workspace picker modal */} - {}} - title={i18n.t("black.workspace.selectPlan")} - variant="black" - > -
-
    { - if (e.key === "Enter" && active()) { - handleSelectWorkspace(active()!) - } else { - onKeyDown(e) - } - }} - > - - {(workspace) => ( -
  • setActive(workspace.id)} - onClick={() => handleSelectWorkspace(workspace.id)} - > - [*] - {workspace.name || workspace.slug} -
  • - )} -
    -
-
-
-

- {i18n.t("black.finePrint.beforeTerms")} ·{" "} - {i18n.t("black.finePrint.terms")} -

-
-
- ) -} diff --git a/packages/console/app/src/routes/download/[channel]/[platform].ts b/packages/console/app/src/routes/download/[channel]/[platform].ts index 7a4b5ef65e..4a4be13f4b 100644 --- a/packages/console/app/src/routes/download/[channel]/[platform].ts +++ b/packages/console/app/src/routes/download/[channel]/[platform].ts @@ -1,4 +1,5 @@ import type { APIEvent } from "@solidjs/start" +import { waitUntil } from "@opencode-ai/console-resource" import type { DownloadPlatform } from "../types" const prodAssetNames: Record = { @@ -30,14 +31,34 @@ export async function GET({ params: { platform, channel } }: APIEvent) { const assetName = channel === "stable" ? prodAssetNames[platform] : betaAssetNames[platform] if (!assetName) return new Response(null, { status: 404 }) - const resp = await fetch( + const latest = await fetch( `https://github.com/anomalyco/${channel === "stable" ? "opencode" : "opencode-beta"}/releases/latest/download/${assetName}`, + { redirect: "manual" }, ) + const location = latest.headers.get("location") + if (!location) return new Response(null, { status: 502 }) - const downloadName = downloadNames[platform] + const key = new Request(location) + const cache = (caches as CacheStorage & { default: Cache }).default + const cached = await cache.match(key) + if (cached) return download(cached, platform, "HIT") + + const resp = await fetch(location) + if (!resp.ok) return resp + + const headers = new Headers(resp.headers) + headers.set("cache-control", "public, max-age=31536000, immutable") + headers.delete("set-cookie") + const result = new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers }) + waitUntil(cache.put(key, result.clone())) + return download(result, platform, "MISS") +} +function download(resp: Response, platform: string, cache: "HIT" | "MISS") { + const downloadName = downloadNames[platform] const headers = new Headers(resp.headers) if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`) + headers.set("x-opencode-cache", cache) return new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers }) } diff --git a/packages/console/app/src/routes/go/index.css b/packages/console/app/src/routes/go/index.css index 5650f1b2a5..8193441c94 100644 --- a/packages/console/app/src/routes/go/index.css +++ b/packages/console/app/src/routes/go/index.css @@ -21,10 +21,22 @@ } } -@keyframes go-graph-bar { +@keyframes go-graph-reveal { to { + clip-path: inset(0 0 0 0) fill-box; + } +} + +@keyframes go-graph-label { + to { + mask-position: 0 0; opacity: 1; - transform: scaleX(1); + } +} + +@keyframes go-graph-grid { + to { + mask-position: 0 100%; } } @@ -477,6 +489,46 @@ body { } [data-component="limit-graph"] { + --reveal-duration: 950ms; + --bonus-duration: 1500ms; + --digit-easing: cubic-bezier(0.22, 1, 0.36, 1); + --grid-duration: 2000ms; + --grid-easing: cubic-bezier(0.22, 1, 0.36, 1); + --bar-delay: 240ms; + /* Critically damped response, shared by the bars, labels, and rolling digits. */ + --spring-easing: linear( + 0, + 0.02991, + 0.10078, + 0.19179, + 0.28962, + 0.38611, + 0.47651, + 0.55839, + 0.63079, + 0.69365, + 0.74748, + 0.79306, + 0.83131, + 0.86315, + 0.8895, + 0.91117, + 0.92892, + 0.94339, + 0.95515, + 0.96467, + 0.97236, + 0.97855, + 0.98352, + 0.98751, + 0.9907, + 0.99324, + 0.99527, + 0.99689, + 0.99817, + 0.99919, + 1 + ); margin: 0 auto; width: calc(100% - 120px); max-width: calc(100% - 120px); @@ -491,6 +543,7 @@ body { } [data-slot="plot"] { + container-type: inline-size; position: relative; overflow: visible; width: 100%; @@ -515,6 +568,7 @@ body { } [data-slot="xlabels"] [data-xlabel] { + opacity: 0; position: absolute; left: var(--x); top: var(--y); @@ -551,11 +605,26 @@ body { } } + [data-slot="xlabels"] [data-xlabel], + [data-slot="pills"] [data-label], + [data-bonus] { + mask-image: linear-gradient(to right, #000 40%, transparent 60%); + mask-size: 250% 100%; + mask-position: 100% 0; + mask-repeat: no-repeat; + } + [data-slot="pills"] { position: absolute; inset: 0; pointer-events: none; + [data-label] { + display: inline-flex; + align-items: center; + gap: 8px; + } + [data-item] { position: absolute; left: var(--x); @@ -573,7 +642,6 @@ body { font-size: 13px; line-height: 20px; box-sizing: border-box; - opacity: 0; } @media (max-width: 60rem) { @@ -592,17 +660,33 @@ body { white-space: nowrap; } + :is([data-regions], [data-limited]) { + color: var(--color-text-weak); + white-space: nowrap; + + a { + color: inherit; + pointer-events: auto; + } + } + [data-value] { color: var(--color-text-strong); font-weight: 600; + font-variant-numeric: tabular-nums; white-space: nowrap; + + number-flow { + line-height: 1; + } } [data-bonus] { + opacity: 0; color: var(--color-text-weak); font-size: 12px; font-weight: 400; - line-height: 1; + line-height: inherit; white-space: nowrap; @media (max-width: 40rem) { @@ -675,11 +759,22 @@ body { opacity: 0.55; } + [data-grid], + [data-stub] { + mask-image: linear-gradient(to top, #000 48%, transparent 52%); + mask-size: 100% 250%; + mask-position: 0 0; + mask-repeat: no-repeat; + mask-origin: stroke-box; + mask-clip: stroke-box; + } + + [data-animate="bar"], + [data-animate="bonus"] { + clip-path: inset(0 100% 0 0) fill-box; + } + [data-bar] { - transform-box: fill-box; - transform-origin: left center; - opacity: 0; - transform: scaleX(0.02); fill: var(--bar-go); stroke: none; } @@ -688,6 +783,11 @@ body { fill: color-mix(in srgb, var(--bar-go) 50%, transparent); } + [data-item][data-infinite] [data-value] { + font-size: 18px; + line-height: 1; + } + [data-val] { fill: var(--color-text-strong); font-size: 13px; @@ -745,18 +845,65 @@ body { animation-delay: var(--d, 0ms); } - &[data-visible] [data-bar] { - animation: go-graph-bar 560ms cubic-bezier(0.2, 0.7, 0.2, 1) forwards; + &[data-visible] [data-grid], + &[data-visible] [data-stub] { + animation: go-graph-grid var(--grid-duration) var(--grid-easing) forwards; animation-delay: var(--d, 0ms); } - &[data-visible] [data-slot="pills"] [data-item] { - opacity: 1; - transition: opacity 240ms ease; - transition-delay: var(--d, 0ms); + &[data-visible] [data-slot="xlabels"] [data-xlabel] { + animation: go-graph-label 300ms ease-out forwards; + animation-delay: var(--d, 0ms); + } + + &[data-visible] [data-animate="bar"] { + animation: go-graph-reveal var(--reveal-duration) var(--spring-easing) forwards; + animation-delay: calc(var(--bar-delay) + var(--d, 0ms)); + } + + &[data-visible] [data-slot="pills"] [data-label] { + animation: go-graph-label 400ms ease-out forwards; + animation-delay: calc(var(--bar-delay) + var(--d, 0ms) + var(--reveal-duration) * 0.4); + } + + &[data-boosted] [data-animate="bonus"] { + animation: go-graph-reveal var(--bonus-duration) var(--spring-easing) forwards; + animation-delay: var(--bonus-delay); + } + + &[data-boosted] [data-slot="pills"] [data-item][data-promo] { + translate: var(--travel) 0; + transition: translate var(--bonus-duration) var(--spring-easing) var(--bonus-delay); + + @media (max-width: 60rem) { + &[data-edge] { + translate: none; + transition: none; + } + } + } + + &[data-boosted] [data-bonus] { + animation: go-graph-label 400ms ease-out forwards; + animation-delay: var(--bonus-delay); } @media (prefers-reduced-motion: reduce) { + [data-grid], + [data-stub], + &[data-visible] [data-grid], + &[data-visible] [data-stub] { + mask-image: none; + animation: none; + } + + [data-slot="xlabels"] [data-xlabel], + &[data-visible] [data-slot="xlabels"] [data-xlabel] { + opacity: 1; + mask-image: none; + animation: none; + } + [data-animate="line"] { stroke-dashoffset: 0; animation: none; @@ -766,9 +913,12 @@ body { transform: none; animation: none; } - [data-bar] { + [data-animate="bar"], + &[data-visible] [data-animate="bar"], + [data-animate="bonus"], + &[data-boosted] [data-animate="bonus"] { opacity: 1; - transform: none; + clip-path: none; animation: none; } [data-row], @@ -777,8 +927,16 @@ body { transition: none; } - [data-slot="pills"] [data-item] { + [data-slot="pills"] [data-label], + &[data-visible] [data-slot="pills"] [data-label], + [data-bonus], + &[data-boosted] [data-bonus] { opacity: 1; + mask-image: none; + animation: none; + } + + &[data-boosted] [data-slot="pills"] [data-item][data-promo] { transition: none; } } @@ -840,6 +998,78 @@ body { gap: 14px; } } + + @media (max-width: 32rem) { + svg { + overflow: visible; + } + + [data-slot="xlabels"] { + transform: translateY(62px); + + [data-tick="10"] { + display: none; + } + } + + [data-slot="bars"] > [data-model="ox-alpha-free"] { + transform: translateY(39px); + } + + [data-slot="pills"] [data-item][data-edge] { + left: 3.846153846%; + right: auto; + width: calc(100% - 3.846153846%); + max-width: 100%; + height: auto; + padding: 0; + background: none; + line-height: 16px; + gap: 3px 8px; + flex-wrap: wrap; + justify-content: flex-start; + + [data-label] { + max-width: 100%; + flex-wrap: wrap; + gap: 3px 8px; + } + + &[data-model="muse-spark-1.3-contributor"] { + transform: translateY(11px); + + [data-regions] { + flex-basis: 100%; + } + } + + &[data-model="ox-alpha-free"] { + transform: translateY(51px); + } + } + + figcaption { + margin-top: 90px; + } + } + + @media (max-width: 21.25rem) { + [data-slot="xlabels"] { + transform: translateY(100px); + } + + [data-slot="bars"] > [data-model="ox-alpha-free"] { + transform: translateY(58px); + } + + [data-slot="pills"] [data-item][data-edge][data-model="ox-alpha-free"] { + transform: translateY(70px); + } + + figcaption { + margin-top: 128px; + } + } } } diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 599ce2b5a1..3704e9634d 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -1,7 +1,7 @@ import "./index.css" import { createAsync, query } from "@solidjs/router" import { Title, Meta } from "@solidjs/meta" -import { For, createMemo, createSignal, onCleanup, onMount } from "solid-js" +import { For, createMemo } from "solid-js" //import { HttpHeader } from "@solidjs/start" import goLogoLight from "../../asset/go-ornate-light.svg" import goLogoDark from "../../asset/go-ornate-dark.svg" @@ -10,6 +10,7 @@ import { Faq } from "~/component/faq" import { Legal } from "~/component/legal" import { Footer } from "~/component/footer" import { Header } from "~/component/header" +import { LimitsGraph } from "~/component/limits-graph" import { config } from "~/config" import { getLastSeenWorkspaceID } from "../workspace/common" import { IconMiniMax, IconMiMo, IconZai, IconAlibaba, IconDeepSeek } from "~/component/icon" @@ -23,214 +24,34 @@ const checkLoggedIn = query(async () => { }, "checkLoggedIn.get") const models = [ - { name: "Grok 4.5", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" }, + { name: "Grok 4.6", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" }, { name: "GPT 5.6 Luna", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" }, + { name: "GLM-5.3-Flash", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "GLM-5.3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "GLM-5.2", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "GLM-5.1", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "Kimi K3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "Kimi K2.7 Code", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "Kimi K2.6", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "LongCat-2.0", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "MiMo-V2.5-Pro", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "MiMo-V2.5", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "Qwen3.8 Max", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Qwen3.8 Flash", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "Qwen3.7 Max", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "Qwen3.7 Plus", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "Qwen3.6 Plus", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "MiniMax M3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "MiniMax M2.7", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Muse Spark 1.3 Contributor", training: "go.faq.a5.used", retention: "go.faq.a5.notZdr" }, + { name: "Muse Spark 1.2 Contributor", training: "go.faq.a5.used", retention: "go.faq.a5.notZdr" }, { name: "DeepSeek V4 Pro", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "DeepSeek V4 Flash", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Hy4 preview", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "Hy3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Omen Alpha", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, ] as const -function LimitsGraph(props: { href: string }) { - let root!: HTMLElement - const [visible, setVisible] = createSignal(false) - - const i18n = useI18n() - - onMount(() => { - if (typeof IntersectionObserver === "undefined") return setVisible(true) - const observer = new IntersectionObserver( - (entries) => { - const entry = entries[0] - if (!entry?.isIntersecting) return - setVisible(true) - observer.disconnect() - }, - { threshold: 0.35 }, - ) - observer.observe(root) - onCleanup(() => observer.disconnect()) - }) - - const baseline = 100 - const graph = [ - { id: "grok-4.5", name: "Grok 4.5", req: 120, d: "50ms" }, - { id: "kimi-k3", name: "Kimi K3", req: 110, d: "75ms" }, - { id: "qwen3.8-max", name: "Qwen3.8 Max", req: 160, d: "90ms" }, - { id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" }, - { id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" }, - { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "270ms" }, - { id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 4100, baseReq: 2050, d: "290ms" }, - { id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" }, - { id: "hy3", name: "Hy3", req: 4300, d: "320ms" }, - { id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" }, - { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - req: 63300, - baseReq: 31650, - edge: true, - d: "340ms", - }, - ] - - const w = 1040 - const chartW = 720 - const left = 40 - const right = 60 - const top = 18 - const bottom = 44 - const plot = chartW - left - right - - const ratio = (n: number) => n / baseline - const rmax = Math.max(1, ...graph.map((m) => ratio(m.req))) - const log = (n: number) => Math.log10(Math.max(n, 1)) - const base = 24 - const p = 2.2 - const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base) - const ticks = [1, 5, 10, 25, 50, 100, 250].filter((t) => t <= rmax) - const labels = (() => { - const set = new Set() - let last = -Infinity - for (const t of ticks) { - if (t === 1) { - set.add(t) - last = x(t) - continue - } - const pos = x(t) - if (pos - last < 44) continue - set.add(t) - last = pos - } - return set - })() - const shown = ticks.filter((t) => labels.has(t)) - const bh = 8 - const gap = 20 - const step = bh + gap - const gy = (i: number) => top + 22 + step * i - const h = gy(graph.length - 1) + bottom - const my = graph.length < 2 ? gy(0) : (gy(0) + gy(graph.length - 1)) / 2 - const px = (n: number) => `${(n / w) * 100}%` - const py = (n: number) => `${(n / h) * 100}%` - const lx = px(left - 16) - const ty = py(h - 18) - - return ( -
-
- - - - - - - -
- -
-
-
-
- {i18n.t("go.graph.label")} - - {i18n.t("go.graph.usageLimits")} - -
-
-
-
-
- ) -} - export default function Home() { const workspaceID = createAsync(() => checkLoggedIn()) const subscribeUrl = createMemo(() => (workspaceID() ? `/workspace/${workspaceID()}/go` : "/auth")) @@ -364,12 +185,7 @@ export default function Home() { {(part) => { if (part === "{{text}}") return {i18n.t("go.cta.text")} if (part === "{{price}}") { - return ( - - {i18n.t("go.cta.price")} - {i18n.t("go.cta.promo")} - - ) + return {i18n.t("go.cta.price")} } return part }} @@ -504,7 +320,7 @@ export default function Home() {

- Grok 4.5: {i18n.t("go.faq.a5.grokRetention")}{" "} + Grok 4.6: {i18n.t("go.faq.a5.grokRetention")}{" "} {i18n.t("go.faq.a5.learnMore")} @@ -517,6 +333,20 @@ export default function Home() { .

+

+ Muse Spark 1.3 Contributor: {i18n.t("go.faq.a5.museRetention")}{" "} + + {i18n.t("go.faq.a5.learnMore")} + + . +

+

+ Muse Spark 1.2 Contributor: {i18n.t("go.faq.a5.museRetention")}{" "} + + {i18n.t("go.faq.a5.learnMore")} + + . +

DeepSeek V4 Flash: {i18n.t("go.faq.a5.deepseekRetention")}

diff --git a/packages/console/app/src/routes/legal/privacy-policy/index.tsx b/packages/console/app/src/routes/legal/privacy-policy/index.tsx index 4426e0ddd0..f7d3b1fb41 100644 --- a/packages/console/app/src/routes/legal/privacy-policy/index.tsx +++ b/packages/console/app/src/routes/legal/privacy-policy/index.tsx @@ -237,9 +237,8 @@ export default function PrivacyPolicy() {
    -
  • Providing, Customizing and Improving the Services
  • -
  • Marketing the Services
  • -
  • Corresponding with You
  • +
  • Passing through to upstream provider to provide services
  • +
  • Not stored
diff --git a/packages/console/app/src/routes/legal/terms-of-service/index.tsx b/packages/console/app/src/routes/legal/terms-of-service/index.tsx index 7847c44bdc..76544fc841 100644 --- a/packages/console/app/src/routes/legal/terms-of-service/index.tsx +++ b/packages/console/app/src/routes/legal/terms-of-service/index.tsx @@ -21,7 +21,7 @@ export default function TermsOfService() {

Terms of Use

-

Effective date: Mar 6, 2026

+

Effective date: Aug 15, 2026

Welcome to OpenCode. Please read on to learn the rules and restrictions that govern your use of @@ -154,6 +154,11 @@ export default function TermsOfService() { is dangerous, harmful, fraudulent, deceptive, threatening, harassing, defamatory, obscene, or otherwise objectionable; +

  • + creates, maintains, or uses accounts in bulk, or creates, maintains, or uses multiple accounts to + circumvent usage limits, access restrictions, billing obligations, promotions, suspensions, or any + other restriction or policy applicable to the Services; +
  • automatically or programmatically extracts data or Output (defined below);
  • Represent that the Output was human-generated when it was not;
  • diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts index e1e4e6cbd3..05f40e4fd2 100644 --- a/packages/console/app/src/routes/stripe/webhook.ts +++ b/packages/console/app/src/routes/stripe/webhook.ts @@ -205,6 +205,13 @@ export async function POST(input: APIEvent) { } else if (productID === BlackData.productID()) { await Billing.unsubscribeBlack({ subscriptionID }) } + + const latestInvoice = body.data.object.latest_invoice + const invoiceID = typeof latestInvoice === "string" ? latestInvoice : latestInvoice?.id + if (invoiceID) { + const invoice = await Billing.stripe().invoices.retrieve(invoiceID) + if (invoice.status === "open") await Billing.stripe().invoices.voidInvoice(invoiceID) + } } if (body.type === "invoice.payment_succeeded") { if ( diff --git a/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx index c9a72c0879..f1b9bb933a 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx @@ -38,21 +38,25 @@ const setReload = action(async (form: FormData) => { } return json( - await Database.use((tx) => - tx - .update(BillingTable) - .set({ - reload: reloadValue, - ...(reloadAmount !== null ? { reloadAmount } : {}), - ...(reloadTrigger !== null ? { reloadTrigger } : {}), - ...(reloadValue - ? { - reloadError: null, - timeReloadError: null, - } - : {}), - }) - .where(eq(BillingTable.workspaceID, workspaceID)), + await withActor( + () => + Database.use((tx) => + tx + .update(BillingTable) + .set({ + reload: reloadValue, + ...(reloadAmount !== null ? { reloadAmount } : {}), + ...(reloadTrigger !== null ? { reloadTrigger } : {}), + ...(reloadValue + ? { + reloadError: null, + timeReloadError: null, + } + : {}), + }) + .where(eq(BillingTable.workspaceID, workspaceID)), + ), + workspaceID, ), { revalidate: queryBillingInfo.key }, ) diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css b/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css index 14389c42a6..77556d9462 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css @@ -19,6 +19,7 @@ [data-slot="usage-item"] { flex: 1; + min-width: 0; display: flex; flex-direction: column; gap: var(--space-2); @@ -60,6 +61,119 @@ color: var(--color-text-muted); } + [data-slot="usage-details"] { + margin-top: var(--space-1); + } + + [data-slot="usage-details-trigger"] { + display: inline-flex; + align-items: center; + gap: var(--space-2); + width: fit-content; + padding: 0; + border: 0; + background-color: transparent; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + cursor: pointer; + + &:hover:not(:disabled) { + background-color: transparent; + border-color: transparent; + } + + svg { + transition: transform 0.2s ease; + } + + &[aria-expanded="true"] svg { + transform: rotate(180deg); + } + + [data-slot="hide-details"] { + display: none; + } + + &[aria-expanded="true"] [data-slot="show-details"] { + display: none; + } + + &[aria-expanded="true"] [data-slot="hide-details"] { + display: inline; + } + } + + [data-slot="usage-details-content"] { + width: 100%; + margin-top: var(--space-3); + } + + [data-slot="usage-details-loading"] { + width: 100%; + margin-top: var(--space-3); + color: var(--color-text-muted); + font-size: var(--font-size-sm); + } + + [data-slot="usage-details-empty"] { + margin: 0; + color: var(--color-text-muted); + font-size: var(--font-size-sm); + } + + [data-slot="usage-details-table"] { + overflow-x: auto; + border: 1px solid var(--color-border-muted); + border-radius: var(--border-radius-sm); + + table { + width: 100%; + min-width: 28rem; + table-layout: fixed; + border-collapse: collapse; + font-size: var(--font-size-sm); + white-space: nowrap; + } + + th, + td { + width: 25%; + padding: var(--space-2) var(--space-3); + border-bottom: 1px solid var(--color-border-muted); + text-align: end; + font-variant-numeric: tabular-nums; + } + + th { + color: var(--color-text-muted); + font-size: var(--font-size-xs); + font-weight: 500; + text-transform: uppercase; + } + + th:first-child, + td:first-child { + text-align: start; + } + + td:first-child { + max-width: 13rem; + overflow: hidden; + color: var(--color-text); + text-overflow: ellipsis; + } + + tbody tr:last-child td { + border-bottom: 0; + } + + [data-slot="usage-total"] td { + border-top: 1px solid var(--color-border); + color: var(--color-text); + font-weight: 600; + } + } + [data-slot="setting-row"] { display: flex; align-items: center; @@ -106,6 +220,7 @@ [data-slot="setting-row"] { margin-top: 0; + padding-block: var(--space-2); } } diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index da1b053a35..868961777c 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -1,15 +1,19 @@ import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router" import { createStore } from "solid-js/store" -import { createMemo, For, Show } from "solid-js" +import { createMemo, createSignal, For, Show } from "solid-js" import { Modal } from "~/component/modal" import { Billing } from "@opencode-ai/console-core/billing.js" -import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js" -import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js" +import { Database, eq, and, gte, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js" +import { BillingTable, LiteTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js" +import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { Actor } from "@opencode-ai/console-core/actor.js" import { Workspace } from "@opencode-ai/console-core/workspace.js" import { Subscription } from "@opencode-ai/console-core/subscription.js" import { LiteData } from "@opencode-ai/console-core/lite.js" +import { ZenData } from "@opencode-ai/console-core/model.js" +import { getMonthlyBounds, getWeekBounds } from "@opencode-ai/console-core/util/date.js" +import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js" import { withActor } from "~/context/auth.withActor" import { queryBillingInfo } from "../../common" import styles from "./lite-section.module.css" @@ -20,8 +24,12 @@ import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time" import { createReferralFromCookie } from "~/lib/referral-invite" import { getRequestEvent } from "solid-js/web" import { countryFromRequest } from "~/lib/request-country" +import { checkCheckoutRateLimit } from "~/routes/zen/util/redis" -import { IconAlipay, IconUpi } from "~/component/icon" +import { IconAlipay, IconChevron, IconUpi } from "~/component/icon" +import { buildLiteUsageBreakdown, getModelQuotaLimit, getUsagePercent } from "~/lib/lite-usage" + +type LiteUsageWindow = "rolling" | "weekly" | "monthly" export const queryLiteSubscription = query(async (workspaceID: string) => { "use server" @@ -39,6 +47,7 @@ export const queryLiteSubscription = query(async (workspaceID: string) => { timeCreated: LiteTable.timeCreated, lite: BillingTable.lite, region: WorkspaceTable.region, + allowTraining: WorkspaceTable.allow_training, }) .from(BillingTable) .innerJoin(LiteTable, eq(LiteTable.workspaceID, BillingTable.workspaceID)) @@ -50,33 +59,160 @@ export const queryLiteSubscription = query(async (workspaceID: string) => { const limits = LiteData.getLimits() const mine = row.userID === Actor.userID() + const now = new Date() + const rollingCutoff = new Date(now.getTime() - limits.rollingWindow * 3600 * 1000) + const week = getWeekBounds(now) + const month = getMonthlyBounds(now, row.timeCreated) + const rollingActive = !!row.timeRollingUpdated && row.timeRollingUpdated >= rollingCutoff + const weeklyActive = !!row.timeWeeklyUpdated && row.timeWeeklyUpdated >= week.start + const monthlyActive = !!row.timeMonthlyUpdated && row.timeMonthlyUpdated >= month.start + const rollingLimit = centsToMicroCents(limits.rollingLimit * 100) + const weeklyLimit = centsToMicroCents(limits.weeklyLimit * 100) + const monthlyLimit = centsToMicroCents(limits.monthlyLimit * 100) + const rollingUsage = Subscription.analyzeRollingUsage({ + limit: limits.rollingLimit, + window: limits.rollingWindow, + usage: row.rollingUsage ?? 0, + timeUpdated: row.timeRollingUpdated ?? now, + }) + const weeklyUsage = Subscription.analyzeWeeklyUsage({ + limit: limits.weeklyLimit, + usage: row.weeklyUsage ?? 0, + timeUpdated: row.timeWeeklyUpdated ?? now, + }) + const monthlyUsage = Subscription.analyzeMonthlyUsage({ + limit: limits.monthlyLimit, + usage: row.monthlyUsage ?? 0, + timeUpdated: row.timeMonthlyUpdated ?? now, + timeSubscribed: row.timeCreated, + }) return { mine, useBalance: row.lite?.useBalance ?? false, + allowTraining: row.allowTraining ?? false, region: row.region ?? (await Workspace.setDefaultRegion({ country: countryFromRequest(getRequestEvent()?.request) })), - rollingUsage: Subscription.analyzeRollingUsage({ - limit: limits.rollingLimit, - window: limits.rollingWindow, - usage: row.rollingUsage ?? 0, - timeUpdated: row.timeRollingUpdated ?? new Date(), - }), - weeklyUsage: Subscription.analyzeWeeklyUsage({ - limit: limits.weeklyLimit, - usage: row.weeklyUsage ?? 0, - timeUpdated: row.timeWeeklyUpdated ?? new Date(), - }), - monthlyUsage: Subscription.analyzeMonthlyUsage({ - limit: limits.monthlyLimit, - usage: row.monthlyUsage ?? 0, - timeUpdated: row.timeMonthlyUpdated ?? new Date(), - timeSubscribed: row.timeCreated, - }), + rollingUsage: { + ...rollingUsage, + usage: rollingActive ? (row.rollingUsage ?? 0) : 0, + limit: rollingLimit, + usagePercent: getUsagePercent(rollingActive ? (row.rollingUsage ?? 0) : 0, rollingLimit), + }, + weeklyUsage: { + ...weeklyUsage, + usage: weeklyActive ? (row.weeklyUsage ?? 0) : 0, + limit: weeklyLimit, + usagePercent: getUsagePercent(weeklyActive ? (row.weeklyUsage ?? 0) : 0, weeklyLimit), + }, + monthlyUsage: { + ...monthlyUsage, + usage: monthlyActive ? (row.monthlyUsage ?? 0) : 0, + limit: monthlyLimit, + usagePercent: getUsagePercent(monthlyActive ? (row.monthlyUsage ?? 0) : 0, monthlyLimit), + }, } }, workspaceID) }, "lite.subscription.get") +export const queryLiteUsageDetails = query(async (workspaceID: string, window: LiteUsageWindow) => { + "use server" + return withActor(async () => { + if (window !== "rolling" && window !== "weekly" && window !== "monthly") return null + const row = await Database.use((tx) => + tx + .select({ + userID: LiteTable.userID, + rollingUsage: LiteTable.rollingUsage, + weeklyUsage: LiteTable.weeklyUsage, + monthlyUsage: LiteTable.monthlyUsage, + timeRollingUpdated: LiteTable.timeRollingUpdated, + timeWeeklyUpdated: LiteTable.timeWeeklyUpdated, + timeMonthlyUpdated: LiteTable.timeMonthlyUpdated, + timeCreated: LiteTable.timeCreated, + }) + .from(LiteTable) + .where(and(eq(LiteTable.workspaceID, Actor.workspace()), isNull(LiteTable.timeDeleted))) + .then((result) => result[0]), + ) + if (!row || row.userID !== Actor.userID()) return null + + const limits = LiteData.getLimits() + const now = new Date() + const detail = (() => { + if (window === "rolling") { + const active = + !!row.timeRollingUpdated && + row.timeRollingUpdated >= new Date(now.getTime() - limits.rollingWindow * 3600 * 1000) + return { + start: active ? row.timeRollingUpdated! : now, + usage: active ? (row.rollingUsage ?? 0) : 0, + limit: centsToMicroCents(limits.rollingLimit * 100), + } + } + if (window === "weekly") { + const start = getWeekBounds(now).start + return { + start, + usage: row.timeWeeklyUpdated && row.timeWeeklyUpdated >= start ? (row.weeklyUsage ?? 0) : 0, + limit: centsToMicroCents(limits.weeklyLimit * 100), + } + } + const start = getMonthlyBounds(now, row.timeCreated).start + return { + start, + usage: row.timeMonthlyUpdated && row.timeMonthlyUpdated >= start ? (row.monthlyUsage ?? 0) : 0, + limit: centsToMicroCents(limits.monthlyLimit * 100), + } + })() + const modelData = Object.fromEntries( + Object.entries(ZenData.list("lite").models).map(([id, value]) => { + const models = Array.isArray(value) ? value : [value] + const multipliers = new Set(models.map((model) => model.costMultiplier)) + return [id, { name: models[0].name, multiplier: multipliers.size === 1 ? models[0].costMultiplier : undefined }] + }), + ) + const usageRows = await Database.use((tx) => + tx + .select({ + model: UsageTable.model, + multiplier: sql`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier'))`, + cost: sql`SUM(${UsageTable.cost})`, + quotaCost: sql`SUM(CASE WHEN JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier') IS NOT NULL THEN ROUND(${UsageTable.cost} * CAST(JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier')) AS DECIMAL(20, 8))) ELSE 0 END)`, + }) + .from(UsageTable) + .innerJoin(KeyTable, and(eq(KeyTable.id, UsageTable.keyID), eq(KeyTable.workspaceID, UsageTable.workspaceID))) + .where( + and( + eq(UsageTable.workspaceID, Actor.workspace()), + eq(KeyTable.userID, row.userID), + gte(UsageTable.timeCreated, detail.start), + sql`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')) = 'lite'`, + ), + ) + .groupBy(UsageTable.model, sql`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier'))`), + ) + + return buildLiteUsageBreakdown({ + usage: detail.usage, + limit: detail.limit, + sources: usageRows.map((usage) => { + const cost = Number(usage.cost) + const info = modelData[usage.model] + const multiplier = usage.multiplier === null ? info?.multiplier : Number(usage.multiplier) + return { + model: usage.model, + name: info?.name ?? usage.model, + cost, + quotaCost: usage.multiplier === null ? Math.round(cost * (multiplier ?? 1)) : Number(usage.quotaCost), + multiplier, + estimated: usage.multiplier === null, + } + }), + }) + }, workspaceID) +}, "lite.subscription.usage") + type LiteSubscription = Awaited> const createLiteCheckoutUrl = action( @@ -84,6 +220,7 @@ const createLiteCheckoutUrl = action( "use server" return json( await withActor(async () => { + await checkCheckoutRateLimit(Actor.account()) const data = await Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl, method }) await createReferralFromCookie() return { error: undefined, data } @@ -154,7 +291,34 @@ const setGoProviderRouting = action(async (form: FormData) => { ) }, "go.providerRouting.set") -function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) { +const setGoAllowTraining = action(async (form: FormData) => { + "use server" + const workspaceID = form.get("workspaceID") as string | null + if (!workspaceID) return { error: formError.workspaceRequired } + const allowTraining = (form.get("allowTraining") as string | null) === "true" + + return json( + await withActor( + () => + Workspace.update({ allow_training: allowTraining }) + .then(() => ({ error: undefined })) + .catch((e) => ({ error: e.message as string })), + workspaceID, + ), + { revalidate: queryLiteSubscription.key }, + ) +}, "go.allowTraining.set") + +type LiteUsage = NonNullable["rollingUsage"] +type LiteUsageDetailsData = NonNullable>> + +function LiteUsageItem(props: { + id: LiteUsageWindow + label: string + usage: LiteUsage + open: boolean + onToggle: () => void +}) { const i18n = useI18n() return ( @@ -163,17 +327,178 @@ function LiteUsageItem(props: { label: string; usage: { usagePercent: number; re {props.label} {props.usage.usagePercent}%
  • -
    -
    +
    +
    {i18n.t("workspace.lite.subscription.resetsIn")}{" "} {formatResetTime(props.usage.resetInSec, i18n, liteResetTimeKeys)} + 0}> +
    + +
    +
    +
    + ) +} + +function LiteUsageDetails(props: { + id: LiteUsageWindow + label: string + quotaLabel: string + usage: LiteUsageDetailsData +}) { + const i18n = useI18n() + const language = useLanguage() + const money = (amount: number) => + new Intl.NumberFormat(language.tag(language.locale()), { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 4, + }).format(amount / 100_000_000) + const totalPercentage = () => + Number(props.usage.rows.reduce((total, row) => total + row.contributionPercent, 0).toFixed(1)) + + return ( +
    +
    + + + + + + + + + + + + {(row) => { + const quota = getModelQuotaLimit(props.usage.limit, row.multiplier) + return ( + + + + + + + ) + }} + + + + + + +
    {i18n.t("workspace.lite.subscription.model")}{props.label}{props.quotaLabel}{i18n.t("workspace.lite.subscription.contribution")}
    + {row.name} + {row.cost === undefined ? "-" : money(row.cost)}{quota === undefined ? "-" : money(quota)}{row.contributionPercent}%
    {i18n.t("workspace.lite.subscription.total")}{totalPercentage()}%
    +
    ) } +function LiteUsageGroup(props: { lite: NonNullable }) { + const params = useParams() + const i18n = useI18n() + const [open, setOpen] = createSignal() + const [store, setStore] = createStore({ + details: {} as Partial>, + loading: undefined as LiteUsageWindow | undefined, + }) + const items = () => + [ + { + id: "rolling", + label: i18n.t("workspace.lite.subscription.rollingUsage"), + quotaLabel: i18n.t("workspace.lite.subscription.rollingQuota"), + usage: props.lite.rollingUsage, + }, + { + id: "weekly", + label: i18n.t("workspace.lite.subscription.weeklyUsage"), + quotaLabel: i18n.t("workspace.lite.subscription.weeklyQuota"), + usage: props.lite.weeklyUsage, + }, + { + id: "monthly", + label: i18n.t("workspace.lite.subscription.monthlyUsage"), + quotaLabel: i18n.t("workspace.lite.subscription.monthlyQuota"), + usage: props.lite.monthlyUsage, + }, + ] as const + const selected = createMemo(() => items().find((item) => item.id === open())) + + async function toggle(id: LiteUsageWindow) { + if (open() === id) { + setOpen() + return + } + setOpen(id) + if (store.details[id] !== undefined) return + setStore("loading", id) + const details = await queryLiteUsageDetails(params.id!, id).catch(() => null) + setStore("details", id, details) + setStore("loading", (current) => (current === id ? undefined : current)) + } + + return ( + <> +
    + + {(item) => ( + toggle(item.id)} + /> + )} + +
    + + {(item) => { + const details = () => store.details[item().id] + return ( + +
    {i18n.t("workspace.lite.loading")}
    +
    + } + > + {(usage) => ( + + )} +
    + ) + }} + + + ) +} + export function LiteSection(props: { lite: LiteSubscription | undefined }) { const params = useParams() const i18n = useI18n() @@ -186,6 +511,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) { const checkoutSubmission = useSubmission(createLiteCheckoutUrl) const useBalanceSubmission = useSubmission(setLiteUseBalance) const providerRoutingSubmission = useSubmission(setGoProviderRouting) + const allowTrainingSubmission = useSubmission(setGoAllowTraining) const [store, setStore] = createStore({ loading: undefined as undefined | "session" | "checkout" | "alipay" | "upi", showModal: false, @@ -240,11 +566,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) { .
    -
    - - - -
    +

    {i18n.t("workspace.lite.subscription.useBalance")}

    @@ -264,6 +586,20 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {

    {i18n.t("workspace.lite.providers.title")}

    {i18n.t("workspace.lite.providers.description")}

    + +

    {i18n.t("workspace.lite.providers.allowTraining")}

    + + + +

    {i18n.t("workspace.lite.providers.useChina")}

    @@ -304,24 +640,33 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {

    {i18n.t("workspace.lite.promo.modelsTitle")}

      -
    • Grok 4.5
    • +
    • Grok 4.6
    • GPT 5.6 Luna
    • +
    • GLM-5.3-Flash
    • +
    • GLM-5.3
    • GLM-5.2
    • GLM-5.1
    • Kimi K3
    • Kimi K2.7 Code
    • Kimi K2.6
    • +
    • LongCat-2.0
    • MiniMax M3
    • MiniMax M2.7
    • +
    • Muse Spark 1.3 Contributor
    • +
    • Muse Spark 1.2 Contributor
    • Qwen3.8 Max
    • +
    • Qwen3.8 Flash
    • Qwen3.7 Max
    • Qwen3.7 Plus
    • Qwen3.6 Plus
    • DeepSeek V4 Pro
    • DeepSeek V4 Flash
    • +
    • DeepSeek V4 Flash Vision Exp
    • MiMo-V2.5
    • MiMo-V2.5-Pro
    • +
    • Hy4 preview
    • Hy3
    • +
    • Omen Alpha

    {i18n.t("workspace.lite.promo.footer")}

    diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx index 96c91889c1..433e0f9863 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -88,7 +88,7 @@ const updateModel = action(async (form: FormData) => { if (!workspaceID) return { error: formError.workspaceRequired } const enabled = (form.get("enabled") as string | null) === "true" return json( - withActor(async () => { + await withActor(async () => { if (enabled) { await Model.disable({ model }) } else { diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx index d41793dd92..fb315eefd5 100644 --- a/packages/console/app/src/routes/workspace/common.tsx +++ b/packages/console/app/src/routes/workspace/common.tsx @@ -6,6 +6,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js" import { and, Database, desc, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { checkCheckoutRateLimit } from "~/routes/zen/util/redis" export function formatDateForTable(date: Date) { const options: Intl.DateTimeFormatOptions = { @@ -77,7 +78,8 @@ export const createCheckoutUrl = action( return json( await withActor( () => - Billing.generateCheckoutUrl({ amount, successUrl, cancelUrl }) + checkCheckoutRateLimit(Actor.account()) + .then(() => Billing.generateCheckoutUrl({ amount, successUrl, cancelUrl })) .then((data) => ({ error: undefined, data })) .catch((e) => ({ error: e.message as string, diff --git a/packages/console/app/src/routes/zen/util/error.ts b/packages/console/app/src/routes/zen/util/error.ts index 5117815d27..bb87ef6be2 100644 --- a/packages/console/app/src/routes/zen/util/error.ts +++ b/packages/console/app/src/routes/zen/util/error.ts @@ -4,6 +4,7 @@ export class MonthlyLimitError extends Error {} export class UserLimitError extends Error {} export class ModelError extends Error {} export class RegionError extends Error {} +export class DataPolicyError extends Error {} class LimitError extends Error { retryAfter?: number diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 951228c9e7..adbfdecc89 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -22,18 +22,13 @@ import { UserLimitError, ModelError, RegionError, + DataPolicyError, RateLimitError, FreeUsageLimitError, GoUsageLimitError, BlackUsageLimitError, } from "./error" -import { - buildCostChunk, - createBodyConverter, - createStreamPartConverter, - createResponseConverter, - UsageInfo, -} from "./provider/provider" +import { buildCostChunk, createStreamPartConverter, createResponseConverter, UsageInfo } from "./provider/provider" import { anthropicHelper } from "./provider/anthropic" import { googleHelper } from "./provider/google" import { openaiHelper } from "./provider/openai" @@ -51,13 +46,14 @@ import { createModelTpsLimiter } from "./modelTpsLimiter" import { createProviderBudgetTracker } from "./providerBudgetTracker" import { accumulateUsage, HOT_WORKSPACES } from "./usageBatcher" import { Workspace } from "@opencode-ai/console-core/workspace.js" -import { countryFromRequest } from "~/lib/request-country" +import { countryFromRequest, isModelCountryRestricted } from "~/lib/request-country" +import { isPeakPricing } from "./pricing" +import { prepareRequestBody } from "./requestBody" +import { requiresGoTrainingConsent } from "./trainingConsent" +import { proxyInference } from "~/lib/inference-proxy" type ZenData = Awaited> -type RetryOptions = { - excludeProviders: string[] - retryCount: number -} +type PreparedBody = Awaited> type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "lite" | "balance" function resolve(text: string, params?: Record) { @@ -85,8 +81,6 @@ export async function handler( type ProviderInfo = Awaited> type CostInfo = ReturnType - const MAX_FAILOVER_RETRIES = 3 - const MAX_RETRYABLE_STATUS_RETRIES = 3 const dict = i18n(localeFromRequest(input.request)) const t = (key: Key, params?: Record) => resolve(dict[key], params) const ADMIN_WORKSPACES = [ @@ -95,32 +89,53 @@ export async function handler( "wrk_01KKZDKDWCS1VTJF8QTX62DD50", // contributors ] + let requestBody: PreparedBody | undefined try { const url = input.request.url - const body = await input.request.json() - const model = opts.parseModel(url, body) - const variant = opts.parseVariant(url, body) - const isStream = opts.parseIsStream(url, body) + const body = input.request.body + if (!body) throw new Error("Missing request body") + requestBody = opts.format === "google" ? undefined : await prepareRequestBody(body) + const model = opts.format === "google" ? opts.parseModel(url, undefined) : (requestBody?.model ?? "") + const googleStream = opts.format === "google" ? opts.parseIsStream(url, undefined) : undefined const rawIp = input.request.headers.get("x-real-ip") ?? "" const ip = rawIp.includes(":") ? rawIp.split(":").slice(0, 4).join(":") : rawIp const rawZenApiKey = opts.parseApiKey(input.request.headers) const zenApiKey = rawZenApiKey === "public" ? undefined : rawZenApiKey + const zenData = ZenData.list(opts.modelList) + if (opts.modelList === "full" && model) { + // Read routing metadata without running legacy model, auth, or balance checks. + const configured = zenData.models[model] + const entry = Array.isArray(configured) + ? configured.find((entry) => entry.formatFilter === opts.format) + : configured + const response = await proxyInference(input.request, { + provider: entry?.byokProvider, + model: entry?.providers.find((provider) => provider.id === entry.byokProvider)?.model, + body: (providerModel) => requestBody?.stream(providerModel ?? model, false) ?? body, + }).catch(() => { + void (requestBody ? requestBody.cancel() : body.cancel()).catch(() => {}) + return Response.json( + { error: { type: "api_error", message: "Inference routing is unavailable. Please retry later." } }, + { status: 503, headers: { "Cache-Control": "no-store" } }, + ) + }) + if (response) return response + } const sessionId = input.request.headers.get("x-opencode-session") ?? "" const requestId = input.request.headers.get("x-opencode-request") ?? "" const ocClient = input.request.headers.get("x-opencode-client") ?? "" const projectId = input.request.headers.get("x-opencode-project") ?? "" const userAgent = input.request.headers.get("user-agent") ?? "" logger.metric({ - is_stream: isStream, session: sessionId, request: requestId, client: ocClient, user_agent: userAgent, - "model.variant": variant, "model.tier": opts.modelList === "full" ? "zen" : "go", }) - const zenData = ZenData.list(opts.modelList) const modelInfo = validateModel(zenData, model) + const country = countryFromRequest(input.request) + if (isModelCountryRestricted(modelInfo.id, country)) throw new RegionError(t("zen.api.error.countryNotAllowed")) const trialLimiter = createTrialLimiter(modelInfo.trialProvider, ip) const trialProviders = await trialLimiter?.check() const rateLimiter = modelInfo.allowAnonymous @@ -128,12 +143,18 @@ export async function handler( : createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request) await rateLimiter?.check() const authInfo = await authenticate(modelInfo, zenApiKey) + if (authInfo && opts.modelList === "lite" && requiresGoTrainingConsent(modelInfo.id) && !authInfo.allowTraining) + throw new DataPolicyError( + t("zen.api.error.trainingNotAllowed", { + consoleGoUrl: `https://opencode.ai/workspace/${authInfo.workspaceID}/go`, + }), + ) const allowedRegions = authInfo?.region ? authInfo.region : await (async () => { if (!authInfo) return return Actor.provide("system", { workspaceID: authInfo.workspaceID }, () => - Workspace.setDefaultRegion({ country: countryFromRequest(input.request) }), + Workspace.setDefaultRegion({ country }), ) })() if ( @@ -161,7 +182,7 @@ export async function handler( ) const providerBudget = await providerBudgetTracker?.check() - const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => { + const providerRequest = async () => { const providerInfo = selectProvider( model, zenData, @@ -169,7 +190,6 @@ export async function handler( modelInfo, stickyId, trialProviders, - retry, stickyProvider, modelTpmLimits, modelTpsLimits, @@ -185,80 +205,73 @@ export async function handler( }) const startTimestamp = Date.now() - const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream) - const reqBody = JSON.stringify( - providerInfo.modifyBody({ - ...createBodyConverter(opts.format, providerInfo.format)(body), - model: providerInfo.model, - ...(() => { - const replacer = (obj: Record): Record => - Object.fromEntries( - Object.entries(obj).flatMap(([k, v]) => { - if (Array.isArray(v)) return [[k, v]] - if (typeof v === "object") return [[k, replacer(v)]] - if (typeof v === "string") { - if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo.workspaceID]] : [] - if (v === "$org") - return authInfo?.workspaceID ? [[k, authInfo.workspaceID.replace("wrk_", "org_")]] : [] - if (v === "$user") return stickyId ? [[k, stickyId]] : [] - if (v.startsWith("$header.")) { - const headerValue = input.request.headers.get(v.slice(8)) - return headerValue ? [[k, headerValue]] : [] - } - } - return [[k, v]] - }), - ) - return replacer(providerInfo.payloadModifier ?? {}) - })(), - }), - ) + const reqUrl = providerInfo.modifyUrl(providerInfo.api, googleStream ?? false) + const specialAnthropic = + providerInfo.format === "anthropic" && + (providerInfo.model.startsWith("arn:aws:bedrock:") || + providerInfo.model.startsWith("global.anthropic.") || + providerInfo.model.startsWith("databricks-claude-")) + if (providerInfo.format !== opts.format) throw new Error("Zen provider format must match request format") + if (specialAnthropic) throw new Error("Anthropic provider body modifiers are incompatible with streaming") + const prepared = requestBody + + const reqBody = (() => { + if (opts.format === "google") return body + if (!prepared) throw new Error("Missing prepared request body") + return prepared.stream(providerInfo.model, providerInfo.format === "oa-compat") + })() logger.debug("REQUEST URL: " + reqUrl) - logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...") + logger.debug("REQUEST: " + (requestBody?.preview ?? "") + "...") const isNewInference = providerInfo.id.startsWith("console.") || providerInfo.id.startsWith("console-go.") || providerInfo.id.startsWith("inf.") || providerInfo.id.startsWith("inf-go.") - const res = await fetchWithRetryableStatus( - reqUrl, - { - method: "POST", - headers: (() => { - const headers = new Headers(input.request.headers) - providerInfo.modifyHeaders(headers, providerInfo.apiKey, stickyId) - Object.entries(providerInfo.headerModifier ?? {}).forEach(([k, v]) => { - if (v === "$ip") return headers.set(k, ip) - if (v === "$caller") return headers.set(k, stickyId) - if (v === "$session") return headers.set(k, sessionId) - if (v === "$model") return headers.set(k, model) - if (v === "$request") return headers.set(k, requestId) - if (v === "$project") return headers.set(k, projectId) - if (v === "$workspace") { - if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID) - return - } - if (v === "$org") { - if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID.replace("wrk_", "org_")) - return - } - headers.set(k, v) - }) - headers.delete("host") - headers.delete("content-length") - headers.delete("x-opencode-request") + const res = await fetch(reqUrl, { + method: "POST", + headers: (() => { + const headers = new Headers(input.request.headers) + providerInfo.modifyHeaders(headers, providerInfo.apiKey, stickyId) + Object.entries(providerInfo.headerModifier ?? {}).forEach(([k, v]) => { + if (v === "$ip") return headers.set(k, ip) + if (v === "$caller") return headers.set(k, stickyId) + if (v === "$session") return headers.set(k, sessionId) + if (v === "$model") return headers.set(k, model) + if (v === "$request") return headers.set(k, requestId) + if (v === "$client") return headers.set(k, ocClient) + if (v === "$project") return headers.set(k, projectId) + if (v === "$workspace") { + if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID) + return + } + if (v === "$org") { + if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID.replace("wrk_", "org_")) + return + } + headers.set(k, v) + }) + if (isNewInference) { + headers.set("x-zen-model", model) + } + headers.delete("host") + headers.delete("content-length") + if (!isNewInference) { headers.delete("x-opencode-session") headers.delete("x-opencode-project") headers.delete("x-opencode-client") - return headers - })(), - body: reqBody, - // Propagate caller disconnects to the upstream provider request so - // abandoned Console requests do not leave orphaned inference work open. - signal: input.request.signal, - }, - { count: isNewInference ? MAX_RETRYABLE_STATUS_RETRIES : 0 }, - ) + headers.delete("x-opencode-request") + headers.delete("x-zen-model") + } + return headers + })(), + body: reqBody, + duplex: "half", + // Propagate caller disconnects to the upstream provider request so + // abandoned Console requests do not leave orphaned inference work open. + signal: input.request.signal, + } as RequestInit & { duplex: "half" }) + const isStream = res.headers.get("content-type")?.toLowerCase().includes("text/event-stream") ?? false + logger.metric({ is_stream: isStream }) if (isNewInference) { const resEndpointId = res.headers.get("x-opencode-endpoint-id") @@ -277,29 +290,10 @@ export async function handler( }) } - // Try another provider => stop retrying if using fallback provider - if ( - //!isNewInference && - res.status !== 200 && - // ie. 400 error is usually provider error like malformed request - res.status !== 400 && - // ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found. - !(modelInfo.id.startsWith("gpt-") && res.status === 404) && - // ie. cannot change codex model providers mid-session - modelInfo.stickyProvider !== "strict" && - modelInfo.fallbackProvider && - providerInfo.id !== modelInfo.fallbackProvider - ) { - return retriableRequest({ - excludeProviders: [...retry.excludeProviders, providerInfo.id], - retryCount: retry.retryCount + 1, - }) - } - - return { providerInfo, reqBody, res, startTimestamp } + return { providerInfo, res, startTimestamp, isStream } } - const { providerInfo, reqBody, res, startTimestamp } = await retriableRequest() + const { providerInfo, res, startTimestamp, isStream } = await providerRequest() // Store sticky provider if (res.status === 200) await stickyTracker?.set(providerInfo.id) @@ -455,6 +449,8 @@ export async function handler( headers: resHeaders, }) } catch (error: any) { + if (requestBody) void requestBody.cancel().catch(() => {}) + else void input.request.body?.cancel().catch(() => {}) // The caller disconnected before we finished. Because the outbound provider // request shares input.request.signal, an aborted caller surfaces here as an // AbortError. There is no client left to receive a body, so skip the error @@ -477,7 +473,7 @@ export async function handler( } catch {} } - if (error instanceof RegionError) + if (error instanceof RegionError || error instanceof DataPolicyError) return new Response( JSON.stringify({ type: "error", @@ -579,7 +575,6 @@ export async function handler( modelInfo: ModelInfo, stickyId: string, trialProviders: string[] | undefined, - retry: RetryOptions, stickyProviderId: string | undefined, modelTpmLimits: Record | undefined, modelTpsLimits: Record | undefined, @@ -606,14 +601,11 @@ export async function handler( })) } - // Use fallback provider if max retries reached const fallbackProvider = allProviders.find((provider) => provider.id === modelInfo.fallbackProvider) - if (retry.retryCount === MAX_FAILOVER_RETRIES) return fallbackProvider let topPriority = Infinity const providers = allProviders .filter((provider) => provider.weight !== 0) - .filter((provider) => !retry.excludeProviders.includes(provider.id)) .filter((provider) => { if (provider.budgetPriority === undefined) return true if (!providerBudget) return true @@ -708,6 +700,7 @@ export async function handler( workspace: { id: WorkspaceTable.id, region: WorkspaceTable.region, + allowTraining: WorkspaceTable.allow_training, isBlocked: WorkspaceTable.is_blocked, isFlaggedByAnthropic: WorkspaceTable.is_flagged_by_anthropic, isFlaggedByOpenAI: WorkspaceTable.is_flagged_by_openai, @@ -820,6 +813,7 @@ export async function handler( apiKeyId: data.apiKey, workspaceID: data.workspace.id, region: data.workspace.region, + allowTraining: data.workspace.allowTraining ?? false, billing: data.billing, user: data.user, black: data.black, @@ -894,6 +888,8 @@ export async function handler( // Validate lite subscription billing if (opts.modelList === "lite" && authInfo.billing.lite && authInfo.lite) { + if (Object.values(modelInfo.cost).every((price) => price === 0)) return "lite" + try { const consoleGoUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/go` const sub = authInfo.lite @@ -1019,24 +1015,17 @@ export async function handler( providerInfo.apiKey = authInfo.provider.credentials } - async function fetchWithRetryableStatus(url: string, options: RequestInit, retry = { count: 0 }) { - const res = await fetch(url, options) - if ([429, 529].includes(res.status) && retry.count < MAX_RETRYABLE_STATUS_RETRIES) { - await new Promise((resolve) => setTimeout(resolve, Math.pow(2, retry.count) * 500)) - return fetchWithRetryableStatus(url, options, { count: retry.count + 1 }) - } - return res - } - function calculateCost(modelInfo: ModelInfo, usageInfo: UsageInfo) { const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = usageInfo const modelCost = - modelInfo.cost200K && - inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000 - ? modelInfo.cost200K - : modelInfo.cost + modelInfo.costPeak && isPeakPricing(new Date()) + ? modelInfo.costPeak + : modelInfo.cost200K && + inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000 + ? modelInfo.cost200K + : modelInfo.cost const inputCost = modelCost.input * inputTokens * 100 const outputCost = modelCost.output * outputTokens * 100 @@ -1109,6 +1098,8 @@ export async function handler( authInfo = authInfo! const cost = centsToMicroCents(totalCostInCent) + // Keep period bounds and persisted timestamps on one snapshot when a queued write crosses a reset boundary. + const trackedAt = new Date() // For hot workspaces, batch balance/usage updates through Redis to avoid // row-level lock contention on BillingTable/UserTable. Returns the amount @@ -1141,7 +1132,7 @@ export async function handler( enrichment: (() => { if (billingSource === "subscription") return { plan: "sub" } if (billingSource === "byok") return { plan: "byok" } - if (billingSource === "lite") return { plan: "lite" } + if (billingSource === "lite") return { plan: "lite", costMultiplier: modelInfo.costMultiplier } return undefined })(), }), @@ -1149,7 +1140,7 @@ export async function handler( if (billingSource === "subscription") { const plan = authInfo.billing.subscription!.plan const black = BlackData.getLimits({ plan }) - const week = getWeekBounds(new Date()) + const week = getWeekBounds(trackedAt) const rollingWindowSeconds = black.rollingWindow * 3600 return [ db @@ -1157,11 +1148,17 @@ export async function handler( .set({ fixedUsage: sql` CASE + WHEN ${SubscriptionTable.timeFixedUpdated} >= ${week.end} THEN ${SubscriptionTable.fixedUsage} WHEN ${SubscriptionTable.timeFixedUpdated} >= ${week.start} THEN ${SubscriptionTable.fixedUsage} + ${cost} ELSE ${cost} END `, - timeFixedUpdated: sql`now()`, + timeFixedUpdated: sql` + CASE + WHEN ${SubscriptionTable.timeFixedUpdated} > ${trackedAt} THEN ${SubscriptionTable.timeFixedUpdated} + ELSE ${trackedAt} + END + `, rollingUsage: sql` CASE WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.rollingUsage} + ${cost} @@ -1185,8 +1182,8 @@ export async function handler( } if (billingSource === "lite") { const lite = LiteData.getLimits() - const week = getWeekBounds(new Date()) - const month = getMonthlyBounds(new Date(), authInfo.lite!.timeCreated) + const week = getWeekBounds(trackedAt) + const month = getMonthlyBounds(trackedAt, authInfo.lite!.timeCreated) const rollingWindowSeconds = lite.rollingWindow * 3600 const quotaCost = Math.round(cost * modelInfo.costMultiplier) return [ @@ -1195,18 +1192,30 @@ export async function handler( .set({ monthlyUsage: sql` CASE + WHEN ${LiteTable.timeMonthlyUpdated} >= ${month.end} THEN ${LiteTable.monthlyUsage} WHEN ${LiteTable.timeMonthlyUpdated} >= ${month.start} THEN ${LiteTable.monthlyUsage} + ${quotaCost} ELSE ${quotaCost} END `, - timeMonthlyUpdated: sql`now()`, + timeMonthlyUpdated: sql` + CASE + WHEN ${LiteTable.timeMonthlyUpdated} > ${trackedAt} THEN ${LiteTable.timeMonthlyUpdated} + ELSE ${trackedAt} + END + `, weeklyUsage: sql` CASE + WHEN ${LiteTable.timeWeeklyUpdated} >= ${week.end} THEN ${LiteTable.weeklyUsage} WHEN ${LiteTable.timeWeeklyUpdated} >= ${week.start} THEN ${LiteTable.weeklyUsage} + ${quotaCost} ELSE ${quotaCost} END `, - timeWeeklyUpdated: sql`now()`, + timeWeeklyUpdated: sql` + CASE + WHEN ${LiteTable.timeWeeklyUpdated} > ${trackedAt} THEN ${LiteTable.timeWeeklyUpdated} + ELSE ${trackedAt} + END + `, rollingUsage: sql` CASE WHEN UNIX_TIMESTAMP(${LiteTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${LiteTable.rollingUsage} + ${quotaCost} diff --git a/packages/console/app/src/routes/zen/util/pricing.ts b/packages/console/app/src/routes/zen/util/pricing.ts new file mode 100644 index 0000000000..b7b6def61e --- /dev/null +++ b/packages/console/app/src/routes/zen/util/pricing.ts @@ -0,0 +1,11 @@ +export function isPeakPricing(date: Date) { + // DeepSeek peak pricing in China Standard Time (UTC+8): + // - Weekdays only + // - 9 AM to noon + // - 2 PM to 6 PM + const dateCN = new Date(date.getTime() + 8 * 3_600 * 1000) + const dayCN = dateCN.getUTCDay() + if (dayCN === 0 || dayCN === 6) return false + const hourCN = dateCN.getUTCHours() + return (hourCN >= 9 && hourCN < 12) || (hourCN >= 14 && hourCN < 18) +} diff --git a/packages/console/app/src/routes/zen/util/redis.ts b/packages/console/app/src/routes/zen/util/redis.ts index 512523298a..ef4934bd82 100644 --- a/packages/console/app/src/routes/zen/util/redis.ts +++ b/packages/console/app/src/routes/zen/util/redis.ts @@ -16,3 +16,11 @@ export function getRedis() { export function buildRateLimitKey(kind: string, identifier: string, interval?: string) { return `${Resource.App.stage}:ratelimit:${kind}:${identifier}${interval ? `:${interval}` : ""}` } + +export async function checkCheckoutRateLimit(accountID: string) { + const redis = getRedis() + const key = buildRateLimitKey("checkout", accountID) + const count = await redis.incr(key) + if (count === 1) await redis.expire(key, 60 * 60) + if (count > 5) throw new Error("Too many payment attempts. Please try again later.") +} diff --git a/packages/console/app/src/routes/zen/util/requestBody.ts b/packages/console/app/src/routes/zen/util/requestBody.ts new file mode 100644 index 0000000000..a3970dedf1 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/requestBody.ts @@ -0,0 +1,237 @@ +const TAIL_LIMIT = 4 * 1024 +const encoder = new TextEncoder() + +export async function prepareRequestBody(body: ReadableStream) { + const reader = body.getReader() + const chunks: Uint8Array[] = [] + const decoder = new TextDecoder() + let text = "" + let done = false + let bom = 0 + let index = 0 + let depth = 0 + let stringStart = -1 + let escaped = false + let phase: "key" | "colon" | "value" | "comma" = "key" + let key = "" + let found: { model: string; start: number; end: number } | undefined + + const scan = () => { + while (index < text.length && !found) { + const char = text[index] + if (stringStart >= 0) { + if (escaped) escaped = false + else if (char === "\\") escaped = true + else if (char === '"') { + if (depth === 1 && phase === "key") { + key = JSON.parse(text.slice(stringStart, index + 1)) + phase = "colon" + } else if (depth === 1 && phase === "value") { + if (key === "model") { + const start = bom + utf8Length(text, stringStart + 1) + found = { + model: JSON.parse(text.slice(stringStart, index + 1)), + start, + end: bom + utf8Length(text, index), + } + } + phase = "comma" + } + stringStart = -1 + } + index++ + continue + } + + if (char === '"') { + stringStart = index++ + continue + } + if (char === "{" || char === "[") { + if (depth === 1 && phase === "value") phase = "comma" + depth++ + index++ + continue + } + if (char === "}" || char === "]") { + depth-- + index++ + continue + } + if (depth !== 1) { + index++ + continue + } + if (char === ":" && phase === "colon") phase = "value" + else if (char === "," && phase === "comma") phase = "key" + else if (phase === "value" && !/\s/.test(char)) phase = "comma" + index++ + } + } + + while (!done && !found) { + const next = await reader.read() + done = next.done + if (!next.value) continue + if (!chunks.length && next.value[0] === 0xef && next.value[1] === 0xbb && next.value[2] === 0xbf) bom = 3 + chunks.push(next.value) + text += decoder.decode(next.value, { stream: true }) + scan() + } + if (done) { + text += decoder.decode() + scan() + } + + const preview = text.substring(0, 300) + text = "" + let used = false + + return { + model: found?.model ?? "", + preview, + cancel: () => reader.cancel(), + stream(providerModel: string, includeUsage: boolean) { + if (used) throw new Error("Request body stream already consumed") + if (!found) throw new Error("Missing model field") + used = true + + const initial = replace(chunks, found.start, found.end, providerModel) + chunks.length = 0 + const output = passthrough(initial, reader, done) + if (!includeUsage) return output + return appendUsage(output) + }, + } +} + +function utf8Length(value: string, end: number) { + let length = 0 + for (let i = 0; i < end; i++) { + const code = value.charCodeAt(i) + if (code <= 0x7f) length++ + else if (code <= 0x7ff) length += 2 + else if (code >= 0xd800 && code <= 0xdbff && i + 1 < end && value.charCodeAt(i + 1) >= 0xdc00) { + length += 4 + i++ + } else length += 3 + } + return length +} + +function replace(chunks: Uint8Array[], start: number, end: number, value: string) { + let offset = 0 + let inserted = false + return chunks.flatMap((chunk) => { + const chunkStart = offset + const chunkEnd = offset + chunk.length + offset = chunkEnd + if (chunkEnd <= start || chunkStart >= end) return [chunk] + + const parts = [chunk.subarray(0, Math.max(0, start - chunkStart))] + if (!inserted) { + parts.push(encoder.encode(value)) + inserted = true + } + parts.push(chunk.subarray(Math.min(chunk.length, end - chunkStart))) + return parts.filter((part) => part.length) + }) +} + +function passthrough( + initial: Array, + reader: ReadableStreamDefaultReader, + sourceDone: boolean, +) { + let done = sourceDone + let index = 0 + return new ReadableStream({ + async pull(controller) { + const chunk = initial[index] + if (chunk) { + initial[index++] = undefined + controller.enqueue(chunk) + return + } + initial.length = 0 + if (done) { + controller.close() + return + } + const next = await reader.read() + done = next.done + if (next.value) controller.enqueue(next.value) + if (done) controller.close() + }, + cancel(reason) { + initial.length = 0 + return reader.cancel(reason) + }, + }) +} + +function appendUsage(body: ReadableStream) { + const reader = body.getReader() + const decoder = new TextDecoder() + let tail = new Uint8Array() + let streamText = "" + let isStream = false + const inspect = (chunk?: Uint8Array) => { + streamText += chunk ? decoder.decode(chunk, { stream: true }) : decoder.decode() + for (const match of streamText.matchAll(/"stream"\s*:\s*(true|false)/g)) isStream = match[1] === "true" + streamText = streamText.slice(-64) + } + return new ReadableStream({ + async pull(controller) { + while (true) { + const next = await reader.read() + if (next.done) { + inspect() + if (!isStream) { + if (tail.length) controller.enqueue(tail) + controller.close() + return + } + const close = tail.lastIndexOf(125) + if (close < 0) { + controller.error(new Error("Invalid JSON request body")) + return + } + if (close) controller.enqueue(tail.subarray(0, close)) + controller.enqueue(encoder.encode(',"stream_options":{"include_usage":true}}')) + if (close + 1 < tail.length) controller.enqueue(tail.subarray(close + 1)) + controller.close() + return + } + + const chunk = next.value + inspect(chunk) + if (tail.length + chunk.length <= TAIL_LIMIT) { + const combined = new Uint8Array(tail.length + chunk.length) + combined.set(tail) + combined.set(chunk, tail.length) + tail = combined + continue + } + + const emit = tail.length + chunk.length - TAIL_LIMIT + if (emit <= tail.length) { + controller.enqueue(tail.subarray(0, emit)) + const combined = new Uint8Array(TAIL_LIMIT) + combined.set(tail.subarray(emit)) + combined.set(chunk, tail.length - emit) + tail = combined + return + } + + if (tail.length) controller.enqueue(tail) + controller.enqueue(chunk.subarray(0, emit - tail.length)) + tail = chunk.slice(emit - tail.length) + return + } + }, + cancel(reason) { + return reader.cancel(reason) + }, + }) +} diff --git a/packages/console/app/src/routes/zen/util/trainingConsent.ts b/packages/console/app/src/routes/zen/util/trainingConsent.ts new file mode 100644 index 0000000000..adb726f978 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/trainingConsent.ts @@ -0,0 +1,3 @@ +export function requiresGoTrainingConsent(model: string) { + return ["muse-spark-1.3-contributor", "muse-spark-1.2-contributor"].includes(model) +} diff --git a/packages/console/app/src/routes/zen/v1/models.ts b/packages/console/app/src/routes/zen/v1/models.ts index 68c3cac694..262a1bb349 100644 --- a/packages/console/app/src/routes/zen/v1/models.ts +++ b/packages/console/app/src/routes/zen/v1/models.ts @@ -5,14 +5,25 @@ import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js" import { buildOptionsResponse, buildModelsResponse } from "~/routes/zen/util/modelsHandler" +import { Resource } from "@opencode-ai/console-resource" export async function OPTIONS(_input: APIEvent) { return buildOptionsResponse() } export async function GET(input: APIEvent) { + const apiKey = input.request.headers.get("authorization")?.split(" ")[1] + if (apiKey && apiKey !== "public") { + const response = await proxyModels(input, apiKey).catch(() => + Response.json( + { error: { type: "api_error", message: "Inference routing is unavailable. Please retry later." } }, + { status: 503, headers: { "Cache-Control": "no-store" } }, + ), + ) + if (response) return response + } + const disabledModels = await (() => { - const apiKey = input.request.headers.get("authorization")?.split(" ")[1] if (!apiKey) return [] as string[] return Database.use((tx) => @@ -34,3 +45,26 @@ export async function GET(input: APIEvent) { return buildModelsResponse(models) } + +async function proxyModels(input: APIEvent, apiKey: string) { + // No legacy revocation or model-policy checks before destination authentication. + const workspace = await Database.use((tx) => + tx + .select({ migratedAt: WorkspaceTable.migrated_at }) + .from(KeyTable) + .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID)) + .where(eq(KeyTable.key, apiKey)) + .limit(1) + .then((rows) => rows[0]), + ) + if (!workspace?.migratedAt) return undefined + + const destination = new URL(Resource.ConsoleMigration.inferenceUrl) + destination.pathname = `${destination.pathname.replace(/\/$/, "")}/v1/models` + destination.search = new URL(input.request.url).search + destination.hash = "" + const headers = new Headers({ authorization: `Bearer ${apiKey}` }) + const ip = input.request.headers.get("cf-connecting-ip") + if (ip) headers.set("x-real-ip", ip) + return fetch(destination, { headers, signal: input.request.signal, redirect: "manual" }) +} diff --git a/packages/console/app/test/liteUsage.test.ts b/packages/console/app/test/liteUsage.test.ts new file mode 100644 index 0000000000..1d04a04e9e --- /dev/null +++ b/packages/console/app/test/liteUsage.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, test } from "bun:test" +import { buildLiteUsageBreakdown, getModelQuotaLimit } from "../src/lib/lite-usage" + +describe("Go usage breakdown", () => { + test("derives the model quota from the window limit and multiplier", () => { + expect(getModelQuotaLimit(30, 1)).toBe(30) + expect(getModelQuotaLimit(30, 2)).toBe(15) + expect(getModelQuotaLimit(30, 4)).toBe(7.5) + }) + + test("groups model quota usage into the percentage of the limit", () => { + const result = buildLiteUsageBreakdown({ + usage: 416, + limit: 1_200, + sources: [ + { model: "glm", name: "GLM", cost: 200, quotaCost: 300, multiplier: 1.5, estimated: false }, + { model: "kimi", name: "Kimi", cost: 116, quotaCost: 116, multiplier: 1, estimated: false }, + ], + }) + + expect(result.usagePercent).toBe(34.7) + expect(result.rows[0]).toMatchObject({ name: "GLM", multiplier: 1.5, contributionPercent: 25 }) + expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent) + }) + + test("distributes credits across the model contributions", () => { + const result = buildLiteUsageBreakdown({ + usage: 366, + limit: 1_200, + sources: [ + { model: "glm", name: "GLM", cost: 200, quotaCost: 300, multiplier: 1.5, estimated: false }, + { model: "kimi", name: "Kimi", cost: 116, quotaCost: 116, multiplier: 1, estimated: true }, + ], + }) + + expect(result.rows).toHaveLength(2) + expect(result.rows.every((row) => row.contributionPercent >= 0)).toBe(true) + expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent) + }) + + test("does not synthesize a row when request history is unavailable", () => { + const result = buildLiteUsageBreakdown({ usage: 120, limit: 1_200, sources: [] }) + + expect(result.rows).toEqual([]) + }) + + test("allocates rounded percentages without making positive rows negative", () => { + const sources = Array.from({ length: 20 }, (_, index) => ({ + model: `model-${index}`, + name: `Model ${index}`, + cost: 4, + quotaCost: 4, + multiplier: 1, + estimated: false, + })) + const result = buildLiteUsageBreakdown({ usage: 80, limit: 10_000, sources }) + + expect(result.rows.every((row) => row.contributionPercent >= 0)).toBe(true) + expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent) + }) + + test("keeps multiplier changes for the same model as separate rows", () => { + const result = buildLiteUsageBreakdown({ + usage: 500, + limit: 1_000, + sources: [ + { model: "glm", name: "GLM", cost: 100, quotaCost: 100, multiplier: 1, estimated: false }, + { model: "glm", name: "GLM", cost: 200, quotaCost: 400, multiplier: 2, estimated: false }, + ], + }) + + expect(result.rows.map((row) => row.multiplier)).toEqual([2, 1]) + expect(result.rows.map((row) => row.contributionPercent)).toEqual([40, 10]) + }) + + test.each([false, true])("merges same-rate usage (estimated first: %s)", (estimated) => { + const sources = [ + { model: "deepseek-v4-flash", name: "DeepSeek V4 Flash", cost: 200, quotaCost: 400, multiplier: 2, estimated }, + { model: "other", name: "Other", cost: 500, quotaCost: 500, multiplier: 1, estimated: false }, + { + model: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + cost: 100, + quotaCost: 199, + multiplier: 2, + estimated: !estimated, + }, + ] + const original = structuredClone(sources) + const result = buildLiteUsageBreakdown({ usage: 1_050, limit: 6_000, sources }) + + expect(result.rows).toHaveLength(2) + expect(result.rows[0]).toMatchObject({ + model: "deepseek-v4-flash", + cost: 300, + quotaCost: 599, + multiplier: 2, + estimated: true, + }) + expect(getModelQuotaLimit(result.limit, result.rows[0].multiplier)).toBe(3_000) + expect(result.usage).toBe(1_050) + expect(result.usagePercent).toBe(17.5) + expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent) + expect(sources).toEqual(original) + }) + + test("keeps distinct model IDs with the same display name separate", () => { + const result = buildLiteUsageBreakdown({ + usage: 300, + limit: 1_000, + sources: [ + { model: "first", name: "Model", cost: 100, quotaCost: 100, multiplier: 1, estimated: false }, + { model: "second", name: "Model", cost: 200, quotaCost: 200, multiplier: 1, estimated: false }, + ], + }) + + expect(result.rows.map((row) => row.model)).toEqual(["second", "first"]) + }) + + test("does not merge unknown rates with recorded rates", () => { + const result = buildLiteUsageBreakdown({ + usage: 300, + limit: 1_000, + sources: [ + { model: "glm", name: "GLM", cost: 100, quotaCost: 100, estimated: true }, + { model: "glm", name: "GLM", cost: 200, quotaCost: 200, multiplier: 1, estimated: false }, + ], + }) + + expect(result.rows.map((row) => row.multiplier)).toEqual([1, undefined]) + expect(getModelQuotaLimit(result.limit, result.rows[1].multiplier)).toBeUndefined() + }) +}) diff --git a/packages/console/app/test/museSparkPolicy.test.ts b/packages/console/app/test/museSparkPolicy.test.ts new file mode 100644 index 0000000000..e1d92b704d --- /dev/null +++ b/packages/console/app/test/museSparkPolicy.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "bun:test" +import { isModelCountryRestricted } from "../src/lib/request-country" +import { requiresGoTrainingConsent } from "../src/routes/zen/util/trainingConsent" + +describe("Muse Spark model policies", () => { + test.each([ + "muse-spark-1.3-contributor", + "muse-spark-1.3-contributor-free", + "muse-spark-1.2-contributor", + "muse-spark-1.2-contributor-free", + ])("restricts %s in blocked countries", (model) => { + expect(isModelCountryRestricted(model, "CN")).toBe(true) + expect(isModelCountryRestricted(model, "US")).toBe(false) + }) + + test("does not apply the country restriction to similar model IDs", () => { + expect(isModelCountryRestricted("muse-spark-1.3-contributor-preview", "CN")).toBe(false) + }) + + test.each(["muse-spark-1.3-contributor", "muse-spark-1.2-contributor"])( + "requires Go training consent for %s", + (model) => { + expect(requiresGoTrainingConsent(model)).toBe(true) + }, + ) + + test("does not require Go training consent for the free or similar model IDs", () => { + expect(requiresGoTrainingConsent("muse-spark-1.3-contributor-free")).toBe(false) + expect(requiresGoTrainingConsent("muse-spark-1.3-contributor-preview")).toBe(false) + }) +}) diff --git a/packages/console/app/test/pricing.test.ts b/packages/console/app/test/pricing.test.ts new file mode 100644 index 0000000000..2955b3e0f8 --- /dev/null +++ b/packages/console/app/test/pricing.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "bun:test" +import { isPeakPricing } from "../src/routes/zen/util/pricing" + +describe("isPeakPricing", () => { + test.each([ + ["weekday 09:00 CN starts peak pricing", "2026-08-27T01:00:00.000Z", true], + ["weekday 12:00 CN ends peak pricing", "2026-08-27T04:00:00.000Z", false], + ["weekday 14:00 CN starts peak pricing", "2026-08-27T06:00:00.000Z", true], + ["weekday 18:00 CN ends peak pricing", "2026-08-27T10:00:00.000Z", false], + ["Saturday in Beijing", "2026-08-29T01:00:00.000Z", false], + ["Sunday in Beijing", "2026-08-30T06:00:00.000Z", false], + ["Monday in Beijing", "2026-08-31T01:00:00.000Z", true], + ] as const)("handles %s", (_name, timestamp, expected) => { + expect(isPeakPricing(new Date(timestamp))).toBe(expected) + }) +}) diff --git a/packages/console/app/test/requestBody.test.ts b/packages/console/app/test/requestBody.test.ts new file mode 100644 index 0000000000..db3c81b918 --- /dev/null +++ b/packages/console/app/test/requestBody.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, test } from "bun:test" +import { prepareRequestBody } from "../src/routes/zen/util/requestBody" + +describe("Zen request body streaming", () => { + test("patches the leading model without buffering the remaining body", async () => { + let reads = 0 + const body = new ReadableStream( + { + pull(controller) { + const chunks = [ + '{"model":"client-model","stream":true,"messages":[', + JSON.stringify({ role: "user", content: "large payload" }), + "]}", + ] + const chunk = chunks[reads++] + if (chunk) controller.enqueue(new TextEncoder().encode(chunk)) + else controller.close() + }, + }, + { highWaterMark: 0 }, + ) + + const request = await prepareRequestBody(body) + expect(request.model).toBe("client-model") + expect(reads).toBe(1) + + const output = await new Response(request.stream("provider-model", false)).text() + expect(JSON.parse(output)).toEqual({ + model: "provider-model", + stream: true, + messages: [{ role: "user", content: "large payload" }], + }) + }) + + test("ignores model fields nested before the root model", async () => { + const body = new Blob([ + '{"metadata":{"model":"ox-alpha-free"},"model":"glm-5.3","messages":[],"stream":false}', + ]).stream() + const request = await prepareRequestBody(body) + + expect(request.model).toBe("glm-5.3") + expect(JSON.parse(await new Response(request.stream("provider-model", false)).text())).toEqual({ + metadata: { model: "ox-alpha-free" }, + model: "provider-model", + messages: [], + stream: false, + }) + }) + + test("appends stream usage options at the end of the request", async () => { + const body = new Blob(['{"model":"client-model","stream":true,"messages":[]} ']).stream() + const request = await prepareRequestBody(body) + const output = await new Response(request.stream("provider-model", true)).text() + + expect(JSON.parse(output)).toEqual({ + model: "provider-model", + stream: true, + messages: [], + stream_options: { include_usage: true }, + }) + expect(output.endsWith(" ")).toBe(true) + }) + + test("detects streaming after a large message while forwarding", async () => { + const content = "x".repeat(128 * 1024) + let reads = 0 + const chunks = [ + '{"model":"client-model","messages":[', + JSON.stringify({ role: "user", content }), + '],"stream":true}', + ] + const body = new ReadableStream( + { + pull(controller) { + const chunk = chunks[reads++] + if (chunk) controller.enqueue(new TextEncoder().encode(chunk)) + else controller.close() + }, + }, + { highWaterMark: 0 }, + ) + const request = await prepareRequestBody(body) + expect(reads).toBe(1) + const output = await new Response(request.stream("provider-model", true)).text() + + expect(JSON.parse(output)).toEqual({ + model: "provider-model", + messages: [{ role: "user", content }], + stream: true, + stream_options: { include_usage: true }, + }) + }) + + test("buffers through a late model field and then streams the rest", async () => { + const content = "こんにちは".repeat(32 * 1024) + let reads = 0 + const chunks = [ + '{"messages":[', + JSON.stringify({ role: "user", content }), + '],"model":"client-model","stream":true,"extra":"after-model"}', + ] + const body = new ReadableStream( + { + pull(controller) { + const chunk = chunks[reads++] + if (chunk) controller.enqueue(new TextEncoder().encode(chunk)) + else controller.close() + }, + }, + { highWaterMark: 0 }, + ) + const request = await prepareRequestBody(body) + + expect(request.model).toBe("client-model") + expect(reads).toBe(3) + expect(JSON.parse(await new Response(request.stream("provider-model", true)).text())).toEqual({ + messages: [{ role: "user", content }], + model: "provider-model", + stream: true, + extra: "after-model", + stream_options: { include_usage: true }, + }) + }) + + test("preserves a UTF-8 BOM while patching the model", async () => { + const body = new Blob(['\uFEFF{"messages":[],"model":"client-model","stream":false}']).stream() + const request = await prepareRequestBody(body) + const output = new Uint8Array(await new Response(request.stream("provider-model", false)).arrayBuffer()) + + expect([...output.subarray(0, 3)]).toEqual([0xef, 0xbb, 0xbf]) + expect(JSON.parse(new TextDecoder().decode(output))).toEqual({ + messages: [], + model: "provider-model", + stream: false, + }) + }) +}) diff --git a/packages/console/app/test/serverAction.test.ts b/packages/console/app/test/serverAction.test.ts new file mode 100644 index 0000000000..6c9d968128 --- /dev/null +++ b/packages/console/app/test/serverAction.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "bun:test" +import { sanitizeServerActionRequest } from "../src/lib/server-action" + +describe("server action referer", () => { + test("preserves same-origin return locations", () => { + const request = new Request("https://dev.opencode.ai/_server?id=action", { + headers: { referer: "https://dev.opencode.ai/auth?next=%2Fconsole" }, + }) + + expect(sanitizeServerActionRequest(request)).toBe(request) + }) + + test("replaces unsafe return locations with the request origin", () => { + const referers = ["https://evil.example/phishing-login", "not a url", undefined] + + expect( + referers.map((referer) => + sanitizeServerActionRequest( + new Request("https://dev.opencode.ai/_server?id=action", { + headers: referer === undefined ? undefined : { referer }, + }), + ).headers.get("referer"), + ), + ).toEqual(["https://dev.opencode.ai", "https://dev.opencode.ai", "https://dev.opencode.ai"]) + }) + + test("does not change other routes", () => { + const request = new Request("https://dev.opencode.ai/auth", { + headers: { referer: "https://evil.example/phishing-login" }, + }) + + expect(sanitizeServerActionRequest(request)).toBe(request) + }) +}) diff --git a/packages/console/core/migrations/20260819012908_flashy_arclight/migration.sql b/packages/console/core/migrations/20260819012908_flashy_arclight/migration.sql new file mode 100644 index 0000000000..c7741dddaa --- /dev/null +++ b/packages/console/core/migrations/20260819012908_flashy_arclight/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `workspace` ADD `allow_non_zdr` boolean; diff --git a/packages/console/core/migrations/20260819012908_flashy_arclight/snapshot.json b/packages/console/core/migrations/20260819012908_flashy_arclight/snapshot.json new file mode 100644 index 0000000000..e2ae9098d1 --- /dev/null +++ b/packages/console/core/migrations/20260819012908_flashy_arclight/snapshot.json @@ -0,0 +1,3181 @@ +{ + "version": "6", + "dialect": "mysql", + "id": "39a41b57-a092-4356-a8ed-2a51cde2da1d", + "prevIds": ["2752a0ba-95b5-492a-83fd-3dfe7fa77734"], + "ddl": [ + { + "name": "account", + "entityType": "tables" + }, + { + "name": "auth", + "entityType": "tables" + }, + { + "name": "benchmark", + "entityType": "tables" + }, + { + "name": "billing", + "entityType": "tables" + }, + { + "name": "coupon", + "entityType": "tables" + }, + { + "name": "lite", + "entityType": "tables" + }, + { + "name": "payment", + "entityType": "tables" + }, + { + "name": "subscription", + "entityType": "tables" + }, + { + "name": "usage", + "entityType": "tables" + }, + { + "name": "ip_rate_limit", + "entityType": "tables" + }, + { + "name": "ip", + "entityType": "tables" + }, + { + "name": "key_rate_limit", + "entityType": "tables" + }, + { + "name": "model_sticky_provider", + "entityType": "tables" + }, + { + "name": "model_tpm_rate_limit", + "entityType": "tables" + }, + { + "name": "model_tps_rate_limit", + "entityType": "tables" + }, + { + "name": "key", + "entityType": "tables" + }, + { + "name": "model", + "entityType": "tables" + }, + { + "name": "provider", + "entityType": "tables" + }, + { + "name": "referral_code", + "entityType": "tables" + }, + { + "name": "referral_reward", + "entityType": "tables" + }, + { + "name": "referral", + "entityType": "tables" + }, + { + "name": "user", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "account" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "auth" + }, + { + "type": "enum('email','github','google')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subject", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "mediumtext", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "result", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(32)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_type", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(4)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_last4", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "balance", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_trigger", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_amount", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_locked_till", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "enum('20','100','200')", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_plan", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_booked", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_selected", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite_subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "enum('BUILDATHON','GO1MONTH50','GOFREEMONTH','GO3MONTHS100','GO6MONTHS100','GO12MONTHS100')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_redeemed", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "weekly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_weekly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "invoice_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "amount", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_refunded", + "entityType": "columns", + "table": "payment" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "fixed_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_fixed_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "input_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "output_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reasoning_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_read_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_5m_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_1h_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(10)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "ip" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "usage", + "entityType": "columns", + "table": "ip" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "varchar(40)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider_id", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model_tpm_rate_limit" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "model_tpm_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "model_tpm_rate_limit" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "qualify", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "unqualify", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "provider" + }, + { + "type": "text", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "credentials", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "varchar(10)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "code", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "referral_id", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "amount", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_applied", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "referral" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "referral" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "referral" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "referral" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "referral" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "invitee_account_id", + "entityType": "columns", + "table": "referral" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_seen", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "color", + "entityType": "columns", + "table": "user" + }, + { + "type": "enum('admin','member')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "user" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "region", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "allow_non_zdr", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "is_blocked", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "is_flagged_by_anthropic", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "is_flagged_by_openai", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "workspace" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "auth", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "benchmark", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "billing", + "entityType": "pks" + }, + { + "columns": ["email", "type"], + "name": "PRIMARY", + "table": "coupon", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "lite", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "payment", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "subscription", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "usage", + "entityType": "pks" + }, + { + "columns": ["ip", "interval"], + "name": "PRIMARY", + "table": "ip_rate_limit", + "entityType": "pks" + }, + { + "columns": ["ip"], + "name": "PRIMARY", + "table": "ip", + "entityType": "pks" + }, + { + "columns": ["key", "interval"], + "name": "PRIMARY", + "table": "key_rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "model_sticky_provider", + "entityType": "pks" + }, + { + "columns": ["id", "interval"], + "name": "PRIMARY", + "table": "model_tpm_rate_limit", + "entityType": "pks" + }, + { + "columns": ["id", "interval"], + "name": "PRIMARY", + "table": "model_tps_rate_limit", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "key", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "model", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "provider", + "entityType": "pks" + }, + { + "columns": ["workspace_id"], + "name": "PRIMARY", + "table": "referral_code", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "referral_id"], + "name": "PRIMARY", + "table": "referral_reward", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "referral", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "provider", + "isExpression": false + }, + { + "value": "subject", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "provider", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "account_id", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "time_created", + "entityType": "indexes", + "table": "benchmark" + }, + { + "columns": [ + { + "value": "customer_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_customer_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "subscription_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_subscription_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "lite_subscription_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_lite_subscription_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "lite" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "subscription" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "usage_time_created", + "entityType": "indexes", + "table": "usage" + }, + { + "columns": [ + { + "value": "key", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_key", + "entityType": "indexes", + "table": "key" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "model", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "model_workspace_model", + "entityType": "indexes", + "table": "model" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "provider", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_provider", + "entityType": "indexes", + "table": "provider" + }, + { + "columns": [ + { + "value": "code", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "code", + "entityType": "indexes", + "table": "referral_code" + }, + { + "columns": [ + { + "value": "referral_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "referral_id", + "entityType": "indexes", + "table": "referral_reward" + }, + { + "columns": [ + { + "value": "invitee_account_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "invitee_account_id", + "entityType": "indexes", + "table": "referral" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "email", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "email", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "slug", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "slug", + "entityType": "indexes", + "table": "workspace" + } + ], + "renames": [] +} diff --git a/packages/console/core/migrations/20260819031011_oval_morlocks/migration.sql b/packages/console/core/migrations/20260819031011_oval_morlocks/migration.sql new file mode 100644 index 0000000000..310a2240ac --- /dev/null +++ b/packages/console/core/migrations/20260819031011_oval_morlocks/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `workspace` RENAME COLUMN `allow_non_zdr` TO `allow_training`; \ No newline at end of file diff --git a/packages/console/core/migrations/20260819031011_oval_morlocks/snapshot.json b/packages/console/core/migrations/20260819031011_oval_morlocks/snapshot.json new file mode 100644 index 0000000000..30a561a6cc --- /dev/null +++ b/packages/console/core/migrations/20260819031011_oval_morlocks/snapshot.json @@ -0,0 +1,3181 @@ +{ + "version": "6", + "dialect": "mysql", + "id": "e365c1d7-fb02-44ad-b681-87bb51d01964", + "prevIds": ["39a41b57-a092-4356-a8ed-2a51cde2da1d"], + "ddl": [ + { + "name": "account", + "entityType": "tables" + }, + { + "name": "auth", + "entityType": "tables" + }, + { + "name": "benchmark", + "entityType": "tables" + }, + { + "name": "billing", + "entityType": "tables" + }, + { + "name": "coupon", + "entityType": "tables" + }, + { + "name": "lite", + "entityType": "tables" + }, + { + "name": "payment", + "entityType": "tables" + }, + { + "name": "subscription", + "entityType": "tables" + }, + { + "name": "usage", + "entityType": "tables" + }, + { + "name": "ip_rate_limit", + "entityType": "tables" + }, + { + "name": "ip", + "entityType": "tables" + }, + { + "name": "key_rate_limit", + "entityType": "tables" + }, + { + "name": "model_sticky_provider", + "entityType": "tables" + }, + { + "name": "model_tpm_rate_limit", + "entityType": "tables" + }, + { + "name": "model_tps_rate_limit", + "entityType": "tables" + }, + { + "name": "key", + "entityType": "tables" + }, + { + "name": "model", + "entityType": "tables" + }, + { + "name": "provider", + "entityType": "tables" + }, + { + "name": "referral_code", + "entityType": "tables" + }, + { + "name": "referral_reward", + "entityType": "tables" + }, + { + "name": "referral", + "entityType": "tables" + }, + { + "name": "user", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "account" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "auth" + }, + { + "type": "enum('email','github','google')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subject", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "mediumtext", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "result", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(32)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_type", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(4)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_last4", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "balance", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_trigger", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_amount", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_locked_till", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "enum('20','100','200')", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_plan", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_booked", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_selected", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite_subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "enum('BUILDATHON','GO1MONTH50','GOFREEMONTH','GO3MONTHS100','GO6MONTHS100','GO12MONTHS100')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_redeemed", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "weekly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_weekly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "invoice_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "amount", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_refunded", + "entityType": "columns", + "table": "payment" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "fixed_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_fixed_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "input_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "output_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reasoning_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_read_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_5m_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_1h_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(10)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "ip" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "usage", + "entityType": "columns", + "table": "ip" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "varchar(40)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider_id", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model_tpm_rate_limit" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "model_tpm_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "model_tpm_rate_limit" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "qualify", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "unqualify", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "provider" + }, + { + "type": "text", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "credentials", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "varchar(10)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "code", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "referral_id", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "amount", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_applied", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "referral" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "referral" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "referral" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "referral" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "referral" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "invitee_account_id", + "entityType": "columns", + "table": "referral" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_seen", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "color", + "entityType": "columns", + "table": "user" + }, + { + "type": "enum('admin','member')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "user" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "region", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "allow_training", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "is_blocked", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "is_flagged_by_anthropic", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "is_flagged_by_openai", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "workspace" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "auth", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "benchmark", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "billing", + "entityType": "pks" + }, + { + "columns": ["email", "type"], + "name": "PRIMARY", + "table": "coupon", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "lite", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "payment", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "subscription", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "usage", + "entityType": "pks" + }, + { + "columns": ["ip", "interval"], + "name": "PRIMARY", + "table": "ip_rate_limit", + "entityType": "pks" + }, + { + "columns": ["ip"], + "name": "PRIMARY", + "table": "ip", + "entityType": "pks" + }, + { + "columns": ["key", "interval"], + "name": "PRIMARY", + "table": "key_rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "model_sticky_provider", + "entityType": "pks" + }, + { + "columns": ["id", "interval"], + "name": "PRIMARY", + "table": "model_tpm_rate_limit", + "entityType": "pks" + }, + { + "columns": ["id", "interval"], + "name": "PRIMARY", + "table": "model_tps_rate_limit", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "key", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "model", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "provider", + "entityType": "pks" + }, + { + "columns": ["workspace_id"], + "name": "PRIMARY", + "table": "referral_code", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "referral_id"], + "name": "PRIMARY", + "table": "referral_reward", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "referral", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "provider", + "isExpression": false + }, + { + "value": "subject", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "provider", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "account_id", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "time_created", + "entityType": "indexes", + "table": "benchmark" + }, + { + "columns": [ + { + "value": "customer_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_customer_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "subscription_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_subscription_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "lite_subscription_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_lite_subscription_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "lite" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "subscription" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "usage_time_created", + "entityType": "indexes", + "table": "usage" + }, + { + "columns": [ + { + "value": "key", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_key", + "entityType": "indexes", + "table": "key" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "model", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "model_workspace_model", + "entityType": "indexes", + "table": "model" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "provider", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_provider", + "entityType": "indexes", + "table": "provider" + }, + { + "columns": [ + { + "value": "code", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "code", + "entityType": "indexes", + "table": "referral_code" + }, + { + "columns": [ + { + "value": "referral_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "referral_id", + "entityType": "indexes", + "table": "referral_reward" + }, + { + "columns": [ + { + "value": "invitee_account_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "invitee_account_id", + "entityType": "indexes", + "table": "referral" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "email", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "email", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "slug", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "slug", + "entityType": "indexes", + "table": "workspace" + } + ], + "renames": [] +} diff --git a/packages/console/core/migrations/20260901161032_workspace_migrated_at/migration.sql b/packages/console/core/migrations/20260901161032_workspace_migrated_at/migration.sql new file mode 100644 index 0000000000..87918c7305 --- /dev/null +++ b/packages/console/core/migrations/20260901161032_workspace_migrated_at/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `workspace` ADD `migrated_at` timestamp(3); diff --git a/packages/console/core/migrations/20260901161032_workspace_migrated_at/snapshot.json b/packages/console/core/migrations/20260901161032_workspace_migrated_at/snapshot.json new file mode 100644 index 0000000000..e81037d442 --- /dev/null +++ b/packages/console/core/migrations/20260901161032_workspace_migrated_at/snapshot.json @@ -0,0 +1,3195 @@ +{ + "version": "6", + "dialect": "mysql", + "id": "3e952a24-4137-4944-b7fa-e39e71061235", + "prevIds": ["e365c1d7-fb02-44ad-b681-87bb51d01964"], + "ddl": [ + { + "name": "account", + "entityType": "tables" + }, + { + "name": "auth", + "entityType": "tables" + }, + { + "name": "benchmark", + "entityType": "tables" + }, + { + "name": "billing", + "entityType": "tables" + }, + { + "name": "coupon", + "entityType": "tables" + }, + { + "name": "lite", + "entityType": "tables" + }, + { + "name": "payment", + "entityType": "tables" + }, + { + "name": "subscription", + "entityType": "tables" + }, + { + "name": "usage", + "entityType": "tables" + }, + { + "name": "ip_rate_limit", + "entityType": "tables" + }, + { + "name": "ip", + "entityType": "tables" + }, + { + "name": "key_rate_limit", + "entityType": "tables" + }, + { + "name": "model_sticky_provider", + "entityType": "tables" + }, + { + "name": "model_tpm_rate_limit", + "entityType": "tables" + }, + { + "name": "model_tps_rate_limit", + "entityType": "tables" + }, + { + "name": "key", + "entityType": "tables" + }, + { + "name": "model", + "entityType": "tables" + }, + { + "name": "provider", + "entityType": "tables" + }, + { + "name": "referral_code", + "entityType": "tables" + }, + { + "name": "referral_reward", + "entityType": "tables" + }, + { + "name": "referral", + "entityType": "tables" + }, + { + "name": "user", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "account" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "auth" + }, + { + "type": "enum('email','github','google')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subject", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "mediumtext", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "result", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(32)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_type", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(4)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_last4", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "balance", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_trigger", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_amount", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_locked_till", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "enum('20','100','200')", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_plan", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_booked", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_selected", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite_subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "enum('BUILDATHON','GO1MONTH50','GOFREEMONTH','GO3MONTHS100','GO6MONTHS100','GO12MONTHS100')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_redeemed", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "weekly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_weekly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "invoice_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "amount", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_refunded", + "entityType": "columns", + "table": "payment" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "fixed_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_fixed_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "input_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "output_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reasoning_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_read_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_5m_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_1h_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(10)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "ip" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "usage", + "entityType": "columns", + "table": "ip" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "varchar(40)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider_id", + "entityType": "columns", + "table": "model_sticky_provider" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model_tpm_rate_limit" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "model_tpm_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "model_tpm_rate_limit" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "qualify", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "unqualify", + "entityType": "columns", + "table": "model_tps_rate_limit" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "provider" + }, + { + "type": "text", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "credentials", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "varchar(10)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "code", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "referral_code" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "referral_id", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "amount", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_applied", + "entityType": "columns", + "table": "referral_reward" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "referral" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "referral" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "referral" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "referral" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "referral" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "invitee_account_id", + "entityType": "columns", + "table": "referral" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_seen", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "color", + "entityType": "columns", + "table": "user" + }, + { + "type": "enum('admin','member')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "user" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "region", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "allow_training", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "is_blocked", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "is_flagged_by_anthropic", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "is_flagged_by_openai", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "migrated_at", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "workspace" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "auth", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "benchmark", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "billing", + "entityType": "pks" + }, + { + "columns": ["email", "type"], + "name": "PRIMARY", + "table": "coupon", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "lite", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "payment", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "subscription", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "usage", + "entityType": "pks" + }, + { + "columns": ["ip", "interval"], + "name": "PRIMARY", + "table": "ip_rate_limit", + "entityType": "pks" + }, + { + "columns": ["ip"], + "name": "PRIMARY", + "table": "ip", + "entityType": "pks" + }, + { + "columns": ["key", "interval"], + "name": "PRIMARY", + "table": "key_rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "model_sticky_provider", + "entityType": "pks" + }, + { + "columns": ["id", "interval"], + "name": "PRIMARY", + "table": "model_tpm_rate_limit", + "entityType": "pks" + }, + { + "columns": ["id", "interval"], + "name": "PRIMARY", + "table": "model_tps_rate_limit", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "key", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "model", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "provider", + "entityType": "pks" + }, + { + "columns": ["workspace_id"], + "name": "PRIMARY", + "table": "referral_code", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "referral_id"], + "name": "PRIMARY", + "table": "referral_reward", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "referral", + "entityType": "pks" + }, + { + "columns": ["workspace_id", "id"], + "name": "PRIMARY", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "provider", + "isExpression": false + }, + { + "value": "subject", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "provider", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "account_id", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "time_created", + "entityType": "indexes", + "table": "benchmark" + }, + { + "columns": [ + { + "value": "customer_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_customer_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "subscription_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_subscription_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "lite_subscription_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_lite_subscription_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "lite" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "subscription" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "usage_time_created", + "entityType": "indexes", + "table": "usage" + }, + { + "columns": [ + { + "value": "key", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_key", + "entityType": "indexes", + "table": "key" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "model", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "model_workspace_model", + "entityType": "indexes", + "table": "model" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "provider", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_provider", + "entityType": "indexes", + "table": "provider" + }, + { + "columns": [ + { + "value": "code", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "code", + "entityType": "indexes", + "table": "referral_code" + }, + { + "columns": [ + { + "value": "referral_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "referral_id", + "entityType": "indexes", + "table": "referral_reward" + }, + { + "columns": [ + { + "value": "invitee_account_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "invitee_account_id", + "entityType": "indexes", + "table": "referral" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "email", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "email", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "slug", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "slug", + "entityType": "indexes", + "table": "workspace" + } + ], + "renames": [] +} diff --git a/packages/console/core/package.json b/packages/console/core/package.json index a0a16762b6..23aade046b 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.18.18", + "version": "1.18.29", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index 879cd8c677..adeabd9c73 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -328,7 +328,6 @@ export namespace Billing { return LiteData.threeMonths100Coupon if (coupons.some((coupon) => coupon.type === "GOFREEMONTH" && !coupon.timeRedeemed)) return LiteData.firstMonth100Coupon - if (!coupons.some((coupon) => coupon.type === "GO1MONTH50")) return LiteData.firstMonth50Coupon return undefined })() const createSession = () => diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index f4ac40183f..ffba49003c 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -24,6 +24,7 @@ export namespace ZenData { cost: ModelCostSchema, costMultiplier: z.number().default(1), cost200K: ModelCostSchema.optional(), + costPeak: ModelCostSchema.optional(), allowAnonymous: z.boolean().optional(), byokProvider: z.enum(["openai", "anthropic", "google"]).optional(), stickyProvider: z.enum(["strict", "prefer"]).optional(), diff --git a/packages/console/core/src/quota.ts b/packages/console/core/src/quota.ts new file mode 100644 index 0000000000..856cbaba6e --- /dev/null +++ b/packages/console/core/src/quota.ts @@ -0,0 +1,65 @@ +import { z } from "zod" +import { and, Database, eq, isNull } from "./drizzle" +import { LiteTable, SubscriptionTable } from "./schema/billing.sql" +import { Identifier } from "./identifier" +import { fn } from "./util/fn" + +export namespace Quota { + // Zero all usage counters of one plan for a workspace, using the same + // workspace-scoped addressing as unsubscribeLite/unsubscribeBlack and + // Billing.subtractLiteUsage. Timestamps are left untouched on purpose: a + // zeroed counter with a current-window stamp reads as empty and the next + // write accumulates on top of zero, so the active windows stay undisturbed. + export const reset = fn( + z.object({ + workspaceID: Identifier.schema("workspace"), + plan: z.enum(["lite", "subscription"]), + }), + async (input) => { + if (input.plan === "lite") { + return Database.transaction(async (db) => { + const where = and(eq(LiteTable.workspaceID, input.workspaceID), isNull(LiteTable.timeDeleted)) + const rows = await db + .select({ + rollingUsage: LiteTable.rollingUsage, + weeklyUsage: LiteTable.weeklyUsage, + monthlyUsage: LiteTable.monthlyUsage, + }) + .from(LiteTable) + .where(where) + if (rows.length === 0) throw new Error("No lite usage counters found for workspace") + + await db.update(LiteTable).set({ rollingUsage: 0, weeklyUsage: 0, monthlyUsage: 0 }).where(where) + return { + plan: "lite" as const, + before: { + rollingUsage: rows.reduce((sum, row) => sum + (row.rollingUsage ?? 0), 0), + weeklyUsage: rows.reduce((sum, row) => sum + (row.weeklyUsage ?? 0), 0), + monthlyUsage: rows.reduce((sum, row) => sum + (row.monthlyUsage ?? 0), 0), + }, + } + }) + } + if (input.plan === "subscription") { + return Database.transaction(async (db) => { + const where = and(eq(SubscriptionTable.workspaceID, input.workspaceID), isNull(SubscriptionTable.timeDeleted)) + const rows = await db + .select({ rollingUsage: SubscriptionTable.rollingUsage, fixedUsage: SubscriptionTable.fixedUsage }) + .from(SubscriptionTable) + .where(where) + if (rows.length === 0) throw new Error("No subscription usage counters found for workspace") + + await db.update(SubscriptionTable).set({ rollingUsage: 0, fixedUsage: 0 }).where(where) + return { + plan: "subscription" as const, + before: { + rollingUsage: rows.reduce((sum, row) => sum + (row.rollingUsage ?? 0), 0), + fixedUsage: rows.reduce((sum, row) => sum + (row.fixedUsage ?? 0), 0), + }, + } + }) + } + throw new Error(`Unknown plan: ${input.plan}`) + }, + ) +} diff --git a/packages/console/core/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts index 915646cf3d..c788b6a534 100644 --- a/packages/console/core/src/schema/billing.sql.ts +++ b/packages/console/core/src/schema/billing.sql.ts @@ -53,6 +53,7 @@ export const BillingTable = mysqlTable( ...workspaceIndexes(table), uniqueIndex("global_customer_id").on(table.customerID), uniqueIndex("global_subscription_id").on(table.subscriptionID), + uniqueIndex("global_lite_subscription_id").on(table.liteSubscriptionID), ], ) @@ -128,6 +129,7 @@ export const UsageTable = mysqlTable( sessionID: varchar("session_id", { length: 30 }), enrichment: json("enrichment").$type<{ plan: "sub" | "byok" | "lite" + costMultiplier?: number }>(), }, (table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)], diff --git a/packages/console/core/src/schema/referral.sql.ts b/packages/console/core/src/schema/referral.sql.ts index 9850c92457..be2499ee11 100644 --- a/packages/console/core/src/schema/referral.sql.ts +++ b/packages/console/core/src/schema/referral.sql.ts @@ -1,4 +1,4 @@ -import { bigint, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core" +import { bigint, index, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core" import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types" import { workspaceIndexes } from "./workspace.sql" @@ -31,5 +31,8 @@ export const ReferralRewardTable = mysqlTable( amount: bigint("amount", { mode: "number" }).notNull(), timeApplied: utc("time_applied"), }, - (table) => [primaryKey({ columns: [table.workspaceID, table.referralID] })], + (table) => [ + primaryKey({ columns: [table.workspaceID, table.referralID] }), + index("referral_id").on(table.referralID), + ], ) diff --git a/packages/console/core/src/schema/workspace.sql.ts b/packages/console/core/src/schema/workspace.sql.ts index 4a04755bb4..c71f76bd49 100644 --- a/packages/console/core/src/schema/workspace.sql.ts +++ b/packages/console/core/src/schema/workspace.sql.ts @@ -1,5 +1,5 @@ import { boolean, json, primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core" -import { timestamps, ulid } from "../drizzle/types" +import { timestamps, ulid, utc } from "../drizzle/types" export const WorkspaceTable = mysqlTable( "workspace", @@ -8,9 +8,11 @@ export const WorkspaceTable = mysqlTable( slug: varchar("slug", { length: 255 }), name: varchar("name", { length: 255 }).notNull(), region: json("region").$type<("us" | "eu" | "sg" | "cn")[]>(), + allow_training: boolean(), is_blocked: boolean(), is_flagged_by_anthropic: boolean(), is_flagged_by_openai: boolean(), + migrated_at: utc("migrated_at"), ...timestamps, }, (table) => [uniqueIndex("slug").on(table.slug)], diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts index e3c2e0d9a9..432d4947bd 100644 --- a/packages/console/core/src/workspace.ts +++ b/packages/console/core/src/workspace.ts @@ -62,6 +62,7 @@ export namespace Workspace { z.object({ name: z.string().min(1).max(255).optional(), region: z.array(Region).min(1).optional(), + allow_training: z.boolean().optional(), }), async (input) => { Actor.assertAdmin() @@ -72,6 +73,7 @@ export namespace Workspace { .set({ ...("name" in input ? { name: input.name } : {}), ...("region" in input ? { region: input.region } : {}), + ...("allow_training" in input ? { allow_training: input.allow_training } : {}), }) .where(eq(WorkspaceTable.id, workspaceID)), ) @@ -95,6 +97,18 @@ export namespace Workspace { }, ) + export const unblock = fn( + z.object({ + workspaceID: Identifier.schema("workspace"), + }), + async (input) => { + const result = await Database.use((tx) => + tx.update(WorkspaceTable).set({ is_blocked: false }).where(eq(WorkspaceTable.id, input.workspaceID)), + ) + if (result.rowsAffected === 0) throw new Error("Workspace not found") + }, + ) + export const remove = fn(z.void(), async () => { await Database.use((tx) => tx diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 0e9f2fe40a..3a06de763e 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.18.18", + "version": "1.18.29", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", @@ -11,6 +11,7 @@ "devDependencies": { "@cloudflare/workers-types": "catalog:", "@tsconfig/node22": "22.0.2", + "@types/bun": "catalog:", "@types/node": "catalog:", "openai": "5.11.0", "typescript": "catalog:", diff --git a/packages/console/function/src/auth-redirect.test.ts b/packages/console/function/src/auth-redirect.test.ts new file mode 100644 index 0000000000..b3919dbfac --- /dev/null +++ b/packages/console/function/src/auth-redirect.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "bun:test" +import { isAllowedAuthorizationRedirect } from "./auth-redirect" + +describe("authorization redirect validation", () => { + test("allows registered OpenCode callbacks", () => { + expect(isAllowedAuthorizationRedirect("app", "https://opencode.ai/auth/callback")).toBe(true) + expect(isAllowedAuthorizationRedirect("app", "https://dev.opencode.ai/auth/callback")).toBe(true) + expect(isAllowedAuthorizationRedirect("app", "http://localhost:3000/auth/callback")).toBe(true) + expect(isAllowedAuthorizationRedirect("app", "http://127.0.0.1:3000/auth/callback")).toBe(true) + }) + + test("rejects unregistered clients and external redirects", () => { + expect(isAllowedAuthorizationRedirect("other", "https://opencode.ai/auth/callback")).toBe(false) + expect(isAllowedAuthorizationRedirect("app", "https://evil.example/callback")).toBe(false) + expect(isAllowedAuthorizationRedirect("app", "https://opencode.ai.evil.example/callback")).toBe(false) + expect(isAllowedAuthorizationRedirect("app", "javascript:alert(1)")).toBe(false) + }) +}) diff --git a/packages/console/function/src/auth-redirect.ts b/packages/console/function/src/auth-redirect.ts new file mode 100644 index 0000000000..71203f930d --- /dev/null +++ b/packages/console/function/src/auth-redirect.ts @@ -0,0 +1,18 @@ +export const isAllowedAuthorizationRedirect = (clientID: string, redirectURI: string) => { + if (clientID !== "app") return false + const redirect = (() => { + try { + return new URL(redirectURI) + } catch { + return undefined + } + })() + if (redirect === undefined) return false + if (redirect.hostname === "localhost" || redirect.hostname === "127.0.0.1") { + return redirect.protocol === "http:" || redirect.protocol === "https:" + } + return ( + redirect.protocol === "https:" && + (redirect.hostname === "opencode.ai" || redirect.hostname.endsWith(".opencode.ai")) + ) +} diff --git a/packages/console/function/src/auth.ts b/packages/console/function/src/auth.ts index 6d56b96706..457ccc571d 100644 --- a/packages/console/function/src/auth.ts +++ b/packages/console/function/src/auth.ts @@ -17,6 +17,7 @@ import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.j import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js" import { Identifier } from "@opencode-ai/console-core/identifier.js" +import { isAllowedAuthorizationRedirect } from "./auth-redirect.js" type Env = { AuthStorage: KVNamespace @@ -41,6 +42,17 @@ const MY_THEME: Theme = { export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { + const requestURL = new URL(request.url) + if (requestURL.pathname === "/authorize") { + const redirectURI = requestURL.searchParams.get("redirect_uri") + if ( + redirectURI !== null && + !isAllowedAuthorizationRedirect(requestURL.searchParams.get("client_id") ?? "", redirectURI) + ) { + return new Response("Unauthorized client", { status: 400 }) + } + } + const result = await issuer({ theme: MY_THEME, providers: { @@ -102,6 +114,7 @@ export default { namespace: env.AuthStorage, }), subjects, + allow: ({ clientID, redirectURI }) => Promise.resolve(isAllowedAuthorizationRedirect(clientID, redirectURI)), async success(ctx, response) { console.log(response) diff --git a/packages/console/function/tsconfig.json b/packages/console/function/tsconfig.json index 3218dd7e3e..cf99b89bdd 100644 --- a/packages/console/function/tsconfig.json +++ b/packages/console/function/tsconfig.json @@ -6,6 +6,6 @@ "moduleResolution": "bundler", "jsx": "preserve", "jsxImportSource": "react", - "types": ["@cloudflare/workers-types", "node"] + "types": ["@cloudflare/workers-types", "bun", "node"] } } diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index a8de0cbba2..cb4ceb404d 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.18.18", + "version": "1.18.29", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index e5ea6cf524..815bfd644a 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.18.18", + "version": "1.18.29", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 96c989d6e0..276b0fe8f7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.18", + "version": "1.18.29", "name": "@opencode-ai/core", "type": "module", "license": "MIT", @@ -62,15 +62,15 @@ }, "dependencies": { "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.112", - "@ai-sdk/anthropic": "3.0.82", + "@ai-sdk/amazon-bedrock": "4.0.166", + "@ai-sdk/anthropic": "3.0.111", "@ai-sdk/azure": "3.0.88", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", "@ai-sdk/google": "3.0.73", - "@ai-sdk/google-vertex": "4.0.128", + "@ai-sdk/google-vertex": "4.0.181", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.51", "@ai-sdk/openai": "3.0.84", @@ -101,14 +101,14 @@ "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", "@openrouter/ai-sdk-provider": "2.9.0", - "ai-gateway-provider": "3.1.2", + "ai-gateway-provider": "3.2.0", "bun-pty": "0.4.8", "cross-spawn": "catalog:", "diff": "catalog:", "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.1", + "gitlab-ai-provider": "6.13.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index b604dac664..a1a973b821 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -35,7 +35,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) { const id = setTimeout(() => { const err = new Error("SSE read timed out") ctl.abort(err) - void reader.cancel(err) + reader.cancel(err).catch(() => {}) reject(err) }, ms) diff --git a/packages/core/src/database/migration.ts b/packages/core/src/database/migration.ts index 90dee8acbf..644b22ab7a 100644 --- a/packages/core/src/database/migration.ts +++ b/packages/core/src/database/migration.ts @@ -54,12 +54,39 @@ export function applyOnly(db: Database, input: Migration[]) { if ( yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`) ) { - yield* db.run(sql` - INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed) - SELECT name, ${Date.now()} - FROM ${sql.identifier("__drizzle_migrations")} - WHERE name IS NOT NULL - `) + const named = (yield* db.all<{ name: string }>( + sql`SELECT name FROM pragma_table_info('__drizzle_migrations')`, + )).some((column) => column.name === "name") + + if (named) { + yield* db.run(sql` + INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed) + SELECT name, ${Date.now()} + FROM ${sql.identifier("__drizzle_migrations")} + WHERE name IS NOT NULL + `) + } + + if (!named) { + const entries = yield* db.all<{ created_at: number; prefix: string | null }>(sql` + SELECT created_at, strftime('%Y%m%d%H%M%S', created_at / 1000, 'unixepoch') AS prefix + FROM ${sql.identifier("__drizzle_migrations")} + WHERE created_at IS NOT NULL + `) + + for (const entry of entries) { + const migration = input.find((item) => item.id.startsWith(`${entry.prefix}_`)) + if (!migration) { + return yield* Effect.die( + new Error(`Legacy migration timestamp ${entry.created_at} does not match any known migration`), + ) + } + yield* db.run(sql` + INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed) + VALUES (${migration.id}, ${Date.now()}) + `) + } + } completed = new Set( (yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id), ) diff --git a/packages/core/src/database/migration/20260410174513_workspace-name.ts b/packages/core/src/database/migration/20260410174513_workspace-name.ts index 18483e1cf0..8a8557ec7a 100644 --- a/packages/core/src/database/migration/20260410174513_workspace-name.ts +++ b/packages/core/src/database/migration/20260410174513_workspace-name.ts @@ -5,6 +5,9 @@ export default { id: "20260410174513_workspace-name", up(tx) { return Effect.gen(function* () { + const columns = yield* tx.all<{ name: string }>(`PRAGMA table_info(\`workspace\`)`) + const name = columns.some((column) => column.name === "name") ? "`name`" : "''" + yield* tx.run(`PRAGMA foreign_keys=OFF;`) yield* tx.run(` CREATE TABLE \`__new_workspace\` ( @@ -19,7 +22,7 @@ export default { ); `) yield* tx.run( - `INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`, + `INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, ${name}, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`, ) yield* tx.run(`DROP TABLE \`workspace\`;`) yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`) diff --git a/packages/core/src/opencodez/identity.ts b/packages/core/src/opencodez/identity.ts index e6cf073f4c..0cb54afb10 100644 --- a/packages/core/src/opencodez/identity.ts +++ b/packages/core/src/opencodez/identity.ts @@ -15,3 +15,5 @@ export const enabled = export const cliName = process.env["OPENCODE_CLI_NAME"] || (enabled ? "opencodez" : "opencode") export const appName = process.env["OPENCODE_APP_NAME"] || (enabled ? "opencodez" : "opencode") +export const productName = enabled ? "OpenCodez" : "OpenCode" +export const globalConfigDirectory = `~/.config/${appName}` diff --git a/packages/core/src/opencodez/session.ts b/packages/core/src/opencodez/session.ts index aff23ec758..b47d5adc0a 100644 --- a/packages/core/src/opencodez/session.ts +++ b/packages/core/src/opencodez/session.ts @@ -12,6 +12,7 @@ export type PersistedState = { } const metadataKey = "opencodez" +const selectionLimit = 256 const selections = new Map() const listeners = new Set<() => void>() let pendingSelection: Selection = {} @@ -42,7 +43,7 @@ export function pending() { export function consumePending(sessionID: string) { if (Object.keys(pendingSelection).length === 0) return - selections.set(sessionID, { ...pendingSelection }) + cacheSelection(sessionID, { ...pendingSelection }) pendingSelection = {} notify() } @@ -53,7 +54,7 @@ export function apply(sessionID: string | undefined, selection: Selection, metad notify() return } - selections.set(sessionID, merge(selectionForSession(sessionID, metadata), selection)) + cacheSelection(sessionID, merge(selectionForSession(sessionID, metadata), selection)) notify() } @@ -107,13 +108,21 @@ export function indicatorFromMetadata(input: { export function hydrate(sessionID: string | undefined, metadata?: Record) { if (!sessionID) return - const state = fromMetadata(metadata) - let changed = false - if (state.selection && !sameSelection(selections.get(sessionID), state.selection)) { - selections.set(sessionID, state.selection) - changed = true + const selection = fromMetadata(metadata).selection + if (!selection) { + if (selections.delete(sessionID)) notify() + return + } + if (sameSelection(selections.get(sessionID), selection)) { + cacheSelection(sessionID, selection) + return } - if (changed) notify() + cacheSelection(sessionID, selection) + notify() +} + +export function clear(sessionID: string) { + if (selections.delete(sessionID)) notify() } export function pendingMetadata() { @@ -125,7 +134,7 @@ export function pendingMetadata() { export function metadataWithSessionState(metadata: Record | undefined, sessionID: string | undefined) { if (!sessionID) return metadata ?? {} return withState(metadata, { - selection: cleanSelection(selectionForSession(sessionID, metadata)), + selection: cleanSelection(selections.get(sessionID) ?? fromMetadata(metadata).selection ?? {}), }) } @@ -146,7 +155,7 @@ export function fromMetadata(metadata?: Record): PersistedState } export function withState(metadata: Record | undefined, state: PersistedState) { - const next = { ...(metadata ?? {}) } + const next = { ...metadata } const selection = cleanSelection(state.selection ?? {}) const opencodez = { version: 1, @@ -177,7 +186,21 @@ function effectiveForSelection( } function selectionForSession(sessionID: string, metadata?: Record) { - return selections.get(sessionID) ?? fromMetadata(metadata).selection ?? {} + if (metadata !== undefined) return fromMetadata(metadata).selection ?? {} + const selection = selections.get(sessionID) + if (!selection) return {} + cacheSelection(sessionID, selection) + return selection +} + +function cacheSelection(sessionID: string, selection: Selection) { + selections.delete(sessionID) + selections.set(sessionID, selection) + while (selections.size > selectionLimit) { + const oldest = selections.keys().next().value + if (oldest === undefined) return + selections.delete(oldest) + } } function upstreamSystemPromptID(model: OpenCodezSettings.ModelLike | undefined) { diff --git a/packages/core/src/opencodez/settings.ts b/packages/core/src/opencodez/settings.ts index 24a209ed20..58750689bd 100644 --- a/packages/core/src/opencodez/settings.ts +++ b/packages/core/src/opencodez/settings.ts @@ -5,6 +5,7 @@ export type ConfigLike = Record & { responses?: { system?: string | Record wire?: "legacy" | "codex" + context_window?: number compaction?: { threshold?: number token_limit?: number @@ -27,7 +28,7 @@ export type ModelLike = export const defaults = { compaction: { - // Codex rust-v0.146.0 advertises a 272k context window for the ChatGPT + // Codex rust-v0.153.4 defaults to a 272k working window for the ChatGPT // Responses models even when the general provider catalog is larger. context: 272_000, threshold: 0.9, @@ -44,6 +45,7 @@ export const defaults = { "gpt-5.6-luna": "codex_gpt_5_6_luna_terra", "gpt-5.6-terra": "codex_gpt_5_6_luna_terra", "gpt-5.6-sol": "codex_gpt_5_6_sol", + "gpt-6-astra": "codex_gpt_6_astra", }, } @@ -59,6 +61,10 @@ export function responsesWire(config: ConfigLike | undefined) { return config?.opencodez?.responses?.wire ?? "codex" } +export function responsesContextWindow(config: ConfigLike | undefined) { + return config?.opencodez?.responses?.context_window +} + export function responsesCompaction(config: ConfigLike | undefined) { const configured = config?.opencodez?.responses?.compaction return { diff --git a/packages/core/src/opencodez/version.ts b/packages/core/src/opencodez/version.ts index 1bf81cb01b..4492026fa6 100644 --- a/packages/core/src/opencodez/version.ts +++ b/packages/core/src/opencodez/version.ts @@ -4,6 +4,10 @@ export function normalizeOpenCodezVersion(value: string) { return value.replace(/^v/, "") } +export function isProductionOpenCodezVersion(value: string) { + return /^\d+\.\d+\.\d+\+opencodez\.\d+$/.test(normalizeOpenCodezVersion(value)) +} + export function isCurrentOrNewerOpenCodezVersion(current: string, latest: string) { const currentNormalized = normalizeOpenCodezVersion(current) const latestNormalized = normalizeOpenCodezVersion(latest) diff --git a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts index d416f6f19d..2803cb7a8e 100644 --- a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts +++ b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts @@ -24,9 +24,15 @@ export const CloudflareAIGatewayPlugin = define({ apiKey: config.apiKey, options: gatewayOptions(evt.options, metadata), } as any) - const unified = createUnified({ apiKey: config.apiKey }) evt.sdk = { languageModel(modelID: string) { + // Workers AI is the only first-party provider whose upstream is Cloudflare itself, so it is + // the only one that should receive the Cloudflare token as its upstream Authorization header. + // The Unified API addresses Workers AI both with the explicit "workers-ai/" prefix and as + // bare "@cf/..." ids. Third-party providers must not receive the token; they rely on the + // gateway's stored/BYOK keys instead. + const isWorkersAi = modelID.startsWith("workers-ai/") || modelID.startsWith("@cf/") + const unified = createUnified(isWorkersAi ? { apiKey: config.apiKey } : {}) return gateway(unified(modelID)) }, } diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index f07d72e5f4..7bd78cae41 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -13,7 +13,7 @@ import { ConfigProviderV1 } from "../../v1/config/provider" import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options" import { ConfigV1 } from "../../v1/config/config" -const defaultServer = "https://console.opencode.ai" +const defaultServer = "https://opencode.ai/console" const clientID = "opencode-cli" const methodID = Integration.MethodID.make("device") const RemoteResponse = Schema.Struct({ config: ConfigV1.Info }) @@ -45,9 +45,18 @@ function oauth(http: HttpClient.HttpClient) { authorize: () => Effect.gen(function* () { const device = yield* post(http, `${defaultServer}/auth/device/code`, { client_id: clientID }, Device) + const verification = yield* Effect.try({ + try: () => { + const url = new URL(device.verification_uri_complete, `${defaultServer}/`) + if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("expected HTTP(S)") + return url + }, + catch: (cause) => + new Error(`Invalid device verification URL: ${cause instanceof Error ? cause.message : String(cause)}`), + }) return { mode: "auto" as const, - url: `${defaultServer}${device.verification_uri_complete}`, + url: verification.href, instructions: `Enter code: ${device.user_code}`, callback: poll(http, defaultServer, device.device_code, Duration.seconds(device.interval)), } diff --git a/packages/core/src/plugin/skill.ts b/packages/core/src/plugin/skill.ts index ea723dd89d..0ccc0689f0 100644 --- a/packages/core/src/plugin/skill.ts +++ b/packages/core/src/plugin/skill.ts @@ -7,8 +7,13 @@ import { Effect } from "effect" import { AbsolutePath } from "../schema" import { SkillV2 } from "../skill" import customizeOpencodeContent from "./skill/customize-opencode.md" with { type: "text" } +import { OpenCodezIdentity } from "../opencodez/identity" export const CustomizeOpencodeContent = customizeOpencodeContent + .replaceAll("{{PRODUCT_NAME}}", OpenCodezIdentity.productName) + .replaceAll("{{CLI_NAME}}", OpenCodezIdentity.cliName) + .replaceAll("{{GLOBAL_CONFIG_ROOT}}", OpenCodezIdentity.globalConfigDirectory) +export const CustomizeOpencodeDescription = `Use ONLY when the user is editing or creating ${OpenCodezIdentity.productName}'s own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ${OpenCodezIdentity.globalConfigDirectory}/. Also use when creating or fixing ${OpenCodezIdentity.productName} agents, subagents, commands, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring ${OpenCodezIdentity.productName} itself.` export const Plugin = define({ id: "skill", @@ -19,8 +24,7 @@ export const Plugin = define({ type: "embedded", skill: SkillV2.Info.make({ name: "customize-opencode", - description: - "Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, commands, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself.", + description: CustomizeOpencodeDescription, location: AbsolutePath.make("/builtin/customize-opencode.md"), content: CustomizeOpencodeContent, }), diff --git a/packages/core/src/plugin/skill/customize-opencode.md b/packages/core/src/plugin/skill/customize-opencode.md index 549f15e227..3daac3d8a6 100644 --- a/packages/core/src/plugin/skill/customize-opencode.md +++ b/packages/core/src/plugin/skill/customize-opencode.md @@ -5,9 +5,9 @@ skill's content. --> -# Customizing opencode +# Customizing {{PRODUCT_NAME}} -opencode validates its own config strictly and refuses to start when a field +{{PRODUCT_NAME}} validates its own config strictly and refuses to start when a field is wrong. The shapes below cover the common surface area, but they are a **summary, not the source of truth**. @@ -20,7 +20,7 @@ defaults, and descriptions — lives in the published JSON Schema: If a field is not documented in this skill, or you need to confirm an exact shape before writing config, **fetch that URL and read the schema directly** -rather than guessing. opencode hard-fails on invalid config, so the cost of a +rather than guessing. {{PRODUCT_NAME}} hard-fails on invalid config, so the cost of a wrong shape is a broken startup. Independently, every `opencode.json` should declare @@ -29,25 +29,25 @@ mistakes as they type. ## Applying changes -Config is loaded once when opencode starts and is not hot-reloaded. After +Config is loaded once when {{PRODUCT_NAME}} starts and is not hot-reloaded. After saving changes to `opencode.json`, an agent file, a skill, a plugin, or any -other config-time file, **tell the user to quit and restart opencode** for +other config-time file, **tell the user to quit and restart {{CLI_NAME}}** for the changes to take effect. The running session will keep using the already-loaded config until then. ## Where files live -| Scope | Path | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| Project config | `./opencode.json`, `./opencode.jsonc`, or `.opencode/opencode.json` (opencode walks up from the cwd to the worktree root) | -| Global config | `~/.config/opencode/opencode.json` (NOT `~/.opencode/`) | -| Project agents | `.opencode/agent/.md` or `.opencode/agents/.md` | -| Global agents | `~/.config/opencode/agent(s)/.md` | -| Project commands | `.opencode/command/.md` or `.opencode/commands/.md` | -| Global commands | `~/.config/opencode/command(s)/.md` | -| Project skills | `.opencode/skill(s)//SKILL.md` | -| Global skills | `~/.config/opencode/skill(s)//SKILL.md` | -| External skills (auto-loaded) | `~/.claude/skills//SKILL.md`, `~/.agents/skills//SKILL.md` | +| Scope | Path | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Project config | `./opencode.json`, `./opencode.jsonc`, or `.opencode/opencode.json` ({{PRODUCT_NAME}} walks up from the cwd to the worktree root) | +| Global config | `{{GLOBAL_CONFIG_ROOT}}/opencode.json` or `{{GLOBAL_CONFIG_ROOT}}/opencode.jsonc` (NOT `~/.opencode/`) | +| Project agents | `.opencode/agent/.md` or `.opencode/agents/.md` | +| Global agents | `{{GLOBAL_CONFIG_ROOT}}/agent(s)/.md` | +| Project commands | `.opencode/command/.md` or `.opencode/commands/.md` | +| Global commands | `{{GLOBAL_CONFIG_ROOT}}/command(s)/.md` | +| Project skills | `.opencode/skill(s)//SKILL.md` | +| Global skills | `{{GLOBAL_CONFIG_ROOT}}/skill(s)//SKILL.md` | +| External skills (auto-loaded) | `~/.claude/skills//SKILL.md`, `~/.agents/skills//SKILL.md` | Configs from each scope are deep-merged. Project overrides global. Unknown top-level keys in `opencode.json` are rejected with `ConfigInvalidError`. @@ -160,7 +160,7 @@ Shape notes worth being explicit about: ## Skills -opencode's skill loader scans for `**/SKILL.md` inside skill directories. The +{{PRODUCT_NAME}}'s skill loader scans for `**/SKILL.md` inside skill directories. The file is named `SKILL.md` exactly, and lives in its own folder named after the skill: @@ -276,13 +276,13 @@ file, `disable: true` in frontmatter. ### Built-in agents -opencode ships with `build`, `plan`, `general`, `explore`. Hidden internal agents: +{{PRODUCT_NAME}} ships with `build`, `plan`, `general`, `explore`. Hidden internal agents: `compaction`, `title`, `summary`. To override a built-in's fields, define the same key in `agent: { : { ... } }`. ## Commands -opencode's command loader scans for `**/*.md` inside command directories. The +{{PRODUCT_NAME}}'s command loader scans for `**/*.md` inside command directories. The file is named after the command, and lives directly inside the `command` folder: ``` @@ -298,10 +298,10 @@ agent: build model: anthropic/claude-sonnet-4-6 --- -(command body in markdown: the prompt opencode runs, with $ARGUMENTS for the user's input) +(command body in markdown: the prompt {{PRODUCT_NAME}} runs, with $ARGUMENTS for the user's input) ``` -- `template` is the command body — everything below the frontmatter — and is required: it is the prompt opencode runs when the command is invoked. Do not also put a `template:` key in the frontmatter. +- `template` is the command body — everything below the frontmatter — and is required: it is the prompt {{PRODUCT_NAME}} runs when the command is invoked. Do not also put a `template:` key in the frontmatter. - `$ARGUMENTS` is replaced with everything the user typed after the command; `$1`, `$2`, … pull individual positional arguments. - Optional: `description`, `agent`, `model`, `variant`, `subtask`. @@ -404,7 +404,7 @@ Actions: `"allow"`, `"ask"`, `"deny"`. Per-tool value forms: `"allow"` shorthand (treated as `{"*": "allow"}`), or an object `{ pattern: action }`. Within an object, **insertion order matters**. -opencode evaluates the LAST matching rule, so put broad rules first and narrow +{{PRODUCT_NAME}} evaluates the LAST matching rule, so put broad rules first and narrow rules last. `permission: "allow"` (a string at the top level) is shorthand for "allow @@ -424,10 +424,10 @@ the `plan` agent's permission ruleset (`edit: deny *`). ## Escape hatches -When a user's config is broken and opencode won't start, these env vars help: +When a user's config is broken and {{PRODUCT_NAME}} won't start, these env vars help: - `OPENCODE_DISABLE_PROJECT_CONFIG=1`: skip the project's local `opencode.json` - and start from globals only. Run from the project directory, opencode loads, + and start from globals only. Run from the project directory, {{PRODUCT_NAME}} loads, the user edits the broken file, then they restart without the flag. - `OPENCODE_CONFIG=/path/to/file.json`: load an additional explicit config. - `OPENCODE_CONFIG_CONTENT='{"$schema":"https://opencode.ai/config.json"}'`: @@ -447,7 +447,7 @@ When a user's config is broken and opencode won't start, these env vars help: - For agent, command, skill, and plugin definitions, prefer creating new files in the correct location over inlining everything in `opencode.json`. - If the user's existing config is malformed, point them at the env-var escape - hatches above so they can edit from inside opencode without breaking their + hatches above so they can edit from inside {{PRODUCT_NAME}} without breaking their session. -- After saving any config change, remind the user to quit and restart opencode +- After saving any config change, remind the user to quit and restart {{CLI_NAME}} — running sessions keep using the already-loaded config. diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index ac8ea52d93..7e32ddb603 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -264,7 +264,10 @@ const layer = Layer.effect( }), line: match.line_number, offset: match.absolute_offset, - text: match.lines.text.length > 2_000 ? match.lines.text.slice(0, 2_000) + "..." : match.lines.text, + text: + match.lines.text.length > 2_000 + ? match.lines.text.slice(0, 2_000).replace(/[\uD800-\uDBFF]$/, "") + "..." + : match.lines.text, submatches: match.submatches.map((submatch) => ({ text: submatch.match.text, start: submatch.start, diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index ea4cf04aaa..f714633295 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -202,6 +202,7 @@ export const make = (dependencies: Dependencies) => { .stream( LLM.request({ model: input.model, + http: input.request.http, messages: [Message.user(summaryPrompt)], tools: [], generation: { maxTokens: summaryOutput }, diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index afa60dfa88..792067017d 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -12,7 +12,6 @@ import { SessionMessage } from "./message" import { SessionMessageUpdater } from "./message-updater" import { SessionInput } from "./input" import { WorkspaceV2 } from "../workspace" -import { SessionContextEpoch } from "./context-epoch" import { MessageTable, PartTable, SessionInputTable, SessionMessageTable, SessionTable } from "./sql" import type { DeepMutable } from "../schema" @@ -253,7 +252,6 @@ const layer = Layer.effectDiscard( .where(eq(SessionTable.id, event.data.sessionID)) .run() .pipe(Effect.orDie) - yield* SessionContextEpoch.reset(db, event.data.sessionID) }), ) yield* events.project(SessionV1.Event.Deleted, (event) => @@ -449,7 +447,6 @@ const layer = Layer.effectDiscard( .where(eq(SessionTable.id, event.data.sessionID)) .run() .pipe(Effect.orDie) - yield* SessionContextEpoch.reset(db, event.data.sessionID) }), ) }), diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 72c761e10d..874086a06b 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -204,6 +204,13 @@ const layer = Layer.effect( const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id const request = LLM.request({ model, + http: { + headers: { + "x-session-affinity": session.id, + "X-Session-Id": session.id, + ...(session.parentID ? { "x-parent-session-id": session.parentID } : {}), + }, + }, providerOptions: { openai: { promptCacheKey } }, system: [agent.info?.system, system.baseline] .filter((part): part is string => part !== undefined && part.length > 0) diff --git a/packages/core/src/v1/config/opencodez.ts b/packages/core/src/v1/config/opencodez.ts index 87c03ddfca..988cbfcfd6 100644 --- a/packages/core/src/v1/config/opencodez.ts +++ b/packages/core/src/v1/config/opencodez.ts @@ -19,6 +19,10 @@ export const Responses = Schema.Struct({ wire: Schema.optional(Schema.Literals(["legacy", "codex"])).annotate({ description: "ChatGPT OAuth Responses wire mode (default: codex)", }), + context_window: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))).annotate({ + description: + "Requested ChatGPT OAuth working context window; supported models clamp it to their advertised maximum", + }), compaction: Schema.optional(Compaction).annotate({ description: "ChatGPT OAuth server-side Responses compaction policy", }), diff --git a/packages/core/src/v1/config/provider.ts b/packages/core/src/v1/config/provider.ts index f860a2b4ac..5b6a8133c4 100644 --- a/packages/core/src/v1/config/provider.ts +++ b/packages/core/src/v1/config/provider.ts @@ -108,15 +108,20 @@ export const Info = Schema.Struct({ headerTimeout: Schema.optional( Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({ description: - "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.", + "Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout.", }), ).annotate({ description: - "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.", + "Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout.", }), - chunkTimeout: Schema.optional(PositiveInt).annotate({ + chunkTimeout: Schema.optional( + Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({ + description: + "Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.", + }), + ).annotate({ description: - "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.", + "Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.", }), }), [Schema.Record(Schema.String, Schema.Any)], diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index b381cc7418..464ce2695a 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -8,6 +8,7 @@ import { Effect, Layer } from "effect" import { eq, inArray, sql } from "drizzle-orm" import { DatabaseMigration } from "@opencode-ai/core/database/migration" import { migrations } from "@opencode-ai/core/database/migration.gen" +import workspaceNameMigration from "@opencode-ai/core/database/migration/20260410174513_workspace-name" import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage" import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration/20260601010001_normalize_storage_paths" import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order" @@ -38,6 +39,68 @@ const run = (effect: Effect.Effect) => const makeDb = EffectDrizzleSqlite.makeWithDefaults() describe("DatabaseMigration", () => { + test("defaults missing workspace names while preserving legacy workspace data", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run(sql` + CREATE TABLE workspace ( + id text PRIMARY KEY, + type text NOT NULL, + branch text, + directory text, + extra text, + project_id text NOT NULL + ) + `) + yield* db.run(sql` + INSERT INTO workspace (id, type, branch, directory, extra, project_id) + VALUES ('wrk_legacy', 'remote', 'main', '/repo', '{}', 'proj_legacy') + `) + + yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration]) + + expect(yield* db.get(sql`SELECT id, name, branch, directory, extra FROM workspace`)).toEqual({ + id: "wrk_legacy", + name: "", + branch: "main", + directory: "/repo", + extra: "{}", + }) + }), + ) + }) + + test("imports unnamed legacy Drizzle journal entries by their actual migration timestamps", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run(sql`CREATE TABLE __drizzle_migrations (id integer PRIMARY KEY, hash text, created_at integer)`) + yield* db.run(sql` + INSERT INTO __drizzle_migrations (hash, created_at) + VALUES ('', ${Date.UTC(2026, 3, 10, 17, 45, 13)}) + `) + + yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration]) + + expect(yield* db.all(sql`SELECT id FROM migration`)).toEqual([{ id: "20260410174513_workspace-name" }]) + }), + ) + }) + + test("rejects unknown legacy Drizzle journal timestamps instead of guessing completed migrations", async () => { + await expect( + run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run(sql`CREATE TABLE __drizzle_migrations (id integer PRIMARY KEY, hash text, created_at integer)`) + yield* db.run(sql`INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('', 1234567890000)`) + yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration]) + }), + ), + ).rejects.toThrow("does not match any known migration") + }) + test("serializes concurrent embedded initialization for one database path", async () => { await using tmp = await tmpdir() const filename = path.join(tmp.path, "embedded.sqlite") diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 20af84d02f..e1f8bdd6ec 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -1,5 +1,6 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { HttpClient, HttpClientResponse } from "effect/unstable/http" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" import { EventV2 } from "@opencode-ai/core/event" @@ -14,14 +15,16 @@ import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) -const addPlugin = Effect.fn(function* () { +const addPlugin = Effect.fn(function* (http?: HttpClient.HttpClient) { const plugin = yield* PluginV2.Service const host = yield* PluginHost.make(plugin) const events = yield* EventV2.Service const integration = yield* Integration.Service + const client = yield* HttpClient.HttpClient yield* OpencodePlugin.effect(host).pipe( Effect.provideService(EventV2.Service, events), Effect.provideService(Integration.Service, integration), + Effect.provideService(HttpClient.HttpClient, http ?? client), ) }) @@ -82,6 +85,63 @@ describe("OpencodePlugin", () => { }), ) + it.effect("resolves origin-rooted device verification URLs", () => + Effect.gen(function* () { + const http = HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + Response.json({ + device_code: "device", + user_code: "user", + verification_uri_complete: "/console/device?user_code=user&client_id=opencode-cli", + expires_in: 60, + interval: 60, + }), + ), + ), + ) + yield* addPlugin(http) + const integration = yield* Integration.Service + const attempt = yield* integration.connection.oauth({ + integrationID: Integration.ID.make("opencode"), + methodID: Integration.MethodID.make("device"), + inputs: {}, + }) + expect(attempt.url).toBe("https://opencode.ai/console/device?user_code=user&client_id=opencode-cli") + }), + ) + + it.effect("rejects malformed device verification URLs", () => + Effect.gen(function* () { + const http = HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + Response.json({ + device_code: "device", + user_code: "user", + verification_uri_complete: "http://[::1", + expires_in: 60, + interval: 60, + }), + ), + ), + ) + yield* addPlugin(http) + const integration = yield* Integration.Service + const error = yield* integration.connection + .oauth({ + integrationID: Integration.ID.make("opencode"), + methodID: Integration.MethodID.make("device"), + inputs: {}, + }) + .pipe(Effect.flip) + expect(error).toBeInstanceOf(Integration.AuthorizationError) + expect(String(error.cause)).toContain("Invalid device verification URL") + }), + ) + it.live("loads providers and models from the connected OpenCode server", () => Effect.acquireUseRelease( Effect.sync(() => { diff --git a/packages/core/test/preload.test.ts b/packages/core/test/preload.test.ts new file mode 100644 index 0000000000..cd31307886 --- /dev/null +++ b/packages/core/test/preload.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from "bun:test" + +test("disables public npm security audits", () => { + expect(process.env.NPM_CONFIG_AUDIT).toBe("false") +}) diff --git a/packages/core/test/preload.ts b/packages/core/test/preload.ts index 39b237d70a..7a2a3d2bd2 100644 --- a/packages/core/test/preload.ts +++ b/packages/core/test/preload.ts @@ -1,5 +1,6 @@ import path from "path" process.env.OPENCODE_DB = ":memory:" +process.env.NPM_CONFIG_AUDIT = "false" process.env.OPENCODE_MODELS_PATH = path.join(import.meta.dir, "plugin", "fixtures", "models-dev.json") process.env.OPENCODE_DISABLE_MODELS_FETCH = "true" diff --git a/packages/core/test/ripgrep.test.ts b/packages/core/test/ripgrep.test.ts index 3abce1c02d..5695af0009 100644 --- a/packages/core/test/ripgrep.test.ts +++ b/packages/core/test/ripgrep.test.ts @@ -62,4 +62,24 @@ describe("Ripgrep", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ), ) + it.live("does not split surrogate pairs in oversized line previews", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => + fs.writeFile(path.join(tmp.path, "unicode.txt"), `needle${"x".repeat(1_993)}😀\n`), + ) + + const matches = yield* (yield* Ripgrep.Service).grep({ + cwd: tmp.path, + pattern: "needle", + limit: 10, + }) + + expect(matches[0]?.text).toBe(`needle${"x".repeat(1_993)}...`) + }), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) }) diff --git a/packages/core/test/session-projector.test.ts b/packages/core/test/session-projector.test.ts index 6648ee43c3..7ebcd97314 100644 --- a/packages/core/test/session-projector.test.ts +++ b/packages/core/test/session-projector.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { DateTime, Effect, Schema } from "effect" -import { asc, eq } from "drizzle-orm" +import { asc, eq, sql } from "drizzle-orm" import { Database } from "@opencode-ai/core/database/database" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -22,6 +22,7 @@ import { SessionInput } from "@opencode-ai/core/session/input" import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql" import { testEffect } from "./lib/effect" import { Snapshot } from "@opencode-ai/core/snapshot" +import { Location } from "@opencode-ai/core/location" const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node]))) const sessionsLayer = AppNodeBuilder.build(SessionV2.node, [[SessionExecution.node, SessionExecution.noopLayer]]) @@ -44,6 +45,39 @@ const assistantRow = ( } describe("SessionProjector", () => { + it.effect("projects moved sessions without the transitional context epoch table", () => + Effect.gen(function* () { + const { db } = yield* Database.Service + const events = yield* EventV2.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .run() + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "test", + directory: "/project", + title: "test", + version: "test", + }) + .run() + yield* db.run(sql`DROP TABLE session_context_epoch`) + + yield* events.publish(SessionEvent.Moved, { + sessionID, + timestamp: DateTime.makeUnsafe(1), + location: Location.Ref.make({ directory: AbsolutePath.make("/project/subdir") }), + }) + + expect(yield* db.select({ directory: SessionTable.directory }).from(SessionTable).get()).toEqual({ + directory: "/project/subdir", + }) + }), + ) + it.effect("projects staged, cleared, and committed reverts", () => Effect.gen(function* () { const db = (yield* Database.Service).db diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 57d4456d2d..cc58b43b29 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -703,13 +703,6 @@ describe("SessionRunnerLLM", () => { timestamp: DateTime.makeUnsafe(1), location: Location.Ref.make({ directory: AbsolutePath.make("/moved") }), }) - expect( - yield* db - .select() - .from(SessionContextEpochTable) - .where(eq(SessionContextEpochTable.session_id, sessionID)) - .get(), - ).toBeUndefined() yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) const exit = yield* session.resume(sessionID).pipe(Effect.exit) @@ -1108,6 +1101,16 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests).toHaveLength(2) + expect(requests.map((request) => request.http?.headers)).toEqual([ + { + "x-session-affinity": sessionID, + "X-Session-Id": sessionID, + }, + { + "x-session-affinity": sessionID, + "X-Session-Id": sessionID, + }, + ]) expect(userTexts(requests[0])[0]).toContain("## Objective") expect(userTexts(requests[1])).toHaveLength(1) expect(userTexts(requests[1])[0]).toContain("\n## Objective\n- Preserve the task\n") @@ -2515,6 +2518,43 @@ describe("SessionRunnerLLM", () => { }), ) + it.effect("adds session correlation headers to model requests", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run correlated request" }), resume: false }) + + requests.length = 0 + yield* session.resume(sessionID) + + expect(requests[0]?.http?.headers).toEqual({ + "x-session-affinity": sessionID, + "X-Session-Id": sessionID, + }) + }), + ) + + it.effect("adds the parent session header to child model requests", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const parentID = SessionV2.ID.make("ses_runner_parent") + const { db } = yield* Database.Service + yield* db + .update(SessionTable) + .set({ parent_id: parentID }) + .where(eq(SessionTable.id, sessionID)) + .run() + .pipe(Effect.orDie) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run child request" }), resume: false }) + + requests.length = 0 + yield* session.resume(sessionID) + + expect(requests[0]?.http?.headers?.["x-parent-session-id"]).toBe(parentID) + }), + ) + it.effect("bounds 64-character session prompt cache keys", () => Effect.gen(function* () { yield* setup diff --git a/packages/desktop/package.json b/packages/desktop/package.json index cc41236e18..e652a6a188 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.18.18", + "version": "1.18.29", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index bc27894238..0ffa670143 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.18", + "version": "1.18.29", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 2e901a9540..819f7c6d10 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.18", + "version": "1.18.29", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 54ef9a9c35..038a958579 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.18.18", + "version": "1.18.29", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index 81f4cf2ef4..b8dfeca3d2 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,10 +1,13 @@ { "name": "@opencode-ai/function", - "version": "1.18.18", + "version": "1.18.29", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", "license": "MIT", + "scripts": { + "test": "bun test" + }, "devDependencies": { "@cloudflare/workers-types": "catalog:", "@tsconfig/node22": "22.0.2", diff --git a/packages/function/src/api.ts b/packages/function/src/api.ts index 58c74fe322..e57a567dca 100644 --- a/packages/function/src/api.ts +++ b/packages/function/src/api.ts @@ -5,6 +5,7 @@ import { jwtVerify, createRemoteJWKSet } from "jose" import { createAppAuth } from "@octokit/auth-app" import { Octokit } from "@octokit/rest" import { Resource } from "sst" +import { parseRepositoryClaim } from "./github" type Env = { SYNC_SERVER: DurableObjectNamespace @@ -269,42 +270,41 @@ export default new Hono<{ Bindings: Env }>() // verify token const JWKS = createRemoteJWKSet(new URL(JWKS_URL)) - let owner, repo + let repository: ReturnType try { const { payload } = await jwtVerify(token, JWKS, { issuer: GITHUB_ISSUER, audience: EXPECTED_AUDIENCE, }) - const sub = payload.sub // e.g. 'repo:my-org/my-repo:ref:refs/heads/main' - const parts = sub.split(":")[1].split("/") - owner = parts[0] - repo = parts[1] + repository = parseRepositoryClaim(payload) } catch (err) { console.error("Token verification failed:", err) return c.json({ error: "Invalid or expired token" }, { status: 403 }) } - // Create app JWT token - const auth = createAppAuth({ - appId: Resource.GITHUB_APP_ID.value, - privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value, - }) - const appAuth = await auth({ type: "app" }) - - // Lookup installation - const octokit = new Octokit({ auth: appAuth.token }) - const { data: installation } = await octokit.apps.getRepoInstallation({ - owner, - repo, - }) - - // Get installation token - const installationAuth = await auth({ - type: "installation", - installationId: installation.id, - }) - - return c.json({ token: installationAuth.token }) + try { + const auth = createAppAuth({ + appId: Resource.GITHUB_APP_ID.value, + privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value, + }) + const appAuth = await auth({ type: "app" }) + const octokit = new Octokit({ auth: appAuth.token }) + const { data: installation } = await octokit.apps.getRepoInstallation({ + owner: repository.owner, + repo: repository.repo, + }) + const installationAuth = await auth({ + type: "installation", + installationId: installation.id, + }) + return c.json({ token: installationAuth.token }) + } catch (error) { + console.error("GitHub App token exchange failed:", error) + return c.json( + { error: `Failed to exchange GitHub App token for ${repository.owner}/${repository.repo}` }, + { status: 502 }, + ) + } }) /** * Used by the GitHub action to get GitHub installation access token given user PAT token (used when testing `opencode github run` locally) diff --git a/packages/function/src/github.ts b/packages/function/src/github.ts new file mode 100644 index 0000000000..180d377131 --- /dev/null +++ b/packages/function/src/github.ts @@ -0,0 +1,14 @@ +import type { JWTPayload } from "jose" + +export function parseRepositoryClaim(payload: JWTPayload) { + const claim = payload.repository + if (typeof claim !== "string") throw new Error("Repository claim is missing") + + const parts = claim.split("/") + if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error("Repository claim is invalid") + + return { + owner: parts[0], + repo: parts[1], + } +} diff --git a/packages/function/test/github.test.ts b/packages/function/test/github.test.ts new file mode 100644 index 0000000000..9f5fbac953 --- /dev/null +++ b/packages/function/test/github.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test" +import { parseRepositoryClaim } from "../src/github" + +describe("parseRepositoryClaim", () => { + test("reads repository identity with a legacy subject", () => { + expect( + parseRepositoryClaim({ + repository: "octocat/my-repo", + sub: "repo:octocat/my-repo:ref:refs/heads/main", + }), + ).toEqual({ owner: "octocat", repo: "my-repo" }) + }) + + test("reads repository identity with an immutable subject", () => { + expect( + parseRepositoryClaim({ + repository: "octocat/my-repo", + sub: "repo:octocat@123456/my-repo@456789:ref:refs/heads/main", + }), + ).toEqual({ owner: "octocat", repo: "my-repo" }) + }) + + test("does not depend on a repository path in a customized subject", () => { + expect( + parseRepositoryClaim({ + repository: "octocat/my-repo", + sub: "repository_owner:octocat:repository_visibility:private", + }), + ).toEqual({ owner: "octocat", repo: "my-repo" }) + }) + + test("rejects a missing repository claim", () => { + expect(() => parseRepositoryClaim({})).toThrow("Repository claim is missing") + }) + + test("rejects an invalid repository claim", () => { + expect(() => parseRepositoryClaim({ repository: "octocat" })).toThrow("Repository claim is invalid") + }) +}) diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 8fb6f10921..c12a30fe0c 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.18", + "version": "1.18.29", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index d80684e1e2..7e541f1973 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.18", + "version": "1.18.29", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 5d22aad6e1..b5c8e1d3ac 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.18", + "version": "1.18.29", "name": "opencode", "type": "module", "license": "MIT", @@ -12,8 +12,8 @@ "bench:test": "bun run script/bench-test-suite.ts", "profile:test": "bun run script/profile-test-files.ts", "build": "bun run script/build.ts", - "dev": "bun run --conditions=browser ./src/index.ts", - "dev:temporary": "bun run --conditions=browser ./src/temporary.ts" + "dev": "bun run ./src/index.ts", + "dev:temporary": "bun run ./src/temporary.ts" }, "bin": { "opencode": "./bin/opencode" @@ -56,15 +56,15 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.21.0", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.112", - "@ai-sdk/anthropic": "3.0.82", + "@ai-sdk/amazon-bedrock": "4.0.166", + "@ai-sdk/anthropic": "3.0.111", "@ai-sdk/azure": "3.0.88", "@ai-sdk/cerebras": "2.0.60", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", "@ai-sdk/google": "3.0.73", - "@ai-sdk/google-vertex": "4.0.128", + "@ai-sdk/google-vertex": "4.0.181", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.51", "@ai-sdk/openai": "3.0.84", @@ -111,7 +111,7 @@ "@types/ws": "8.18.1", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", - "ai-gateway-provider": "3.1.2", + "ai-gateway-provider": "3.2.0", "bonjour-service": "1.3.0", "chokidar": "4.0.3", "cross-spawn": "catalog:", @@ -120,7 +120,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.1", + "gitlab-ai-provider": "6.13.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 3fe22e5dfe..44d016c351 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -142,16 +142,16 @@ const targets = singleFlag return false } + // A single native build uses the host ABI. ABI-specific artifacts are + // produced only by the complete release matrix. + if (item.abi !== undefined) return false + // When building for the current platform, prefer a single native binary by default. // Baseline binaries require additional Bun artifacts and can be flaky to download. if (item.avx2 === false) { return baselineFlag } - - // also skip abi-specific builds for the same reason - if (item.abi !== undefined) { - return false - } + if (baselineFlag && item.arch === "x64") return false return true }) @@ -222,6 +222,7 @@ for (const item of targets) { OPENCODE_WORKER_PATH: workerPath, OPENCODE_CHANNEL: `'${Script.channel}'`, OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", + OPENCODEZ_TARGET: `'${name.replace(`${artifactName}-`, "")}'`, ...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}), }, }) diff --git a/packages/opencode/src/account/account.ts b/packages/opencode/src/account/account.ts index 4b49d2a748..fb21f878f8 100644 --- a/packages/opencode/src/account/account.ts +++ b/packages/opencode/src/account/account.ts @@ -396,10 +396,18 @@ const layer: Layer.Layer { + const url = new URL(parsed.verification_uri_complete, `${normalizedServer}/`) + if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("expected HTTP(S)") + return url.href + }, + catch: (cause) => new AccountServiceError({ message: "Invalid device verification URL", cause }), + }) return new Login({ code: parsed.device_code, user: parsed.user_code, - url: `${normalizedServer}${parsed.verification_uri_complete}`, + url: verification, server: normalizedServer, expiry: parsed.expires_in, interval: parsed.interval, diff --git a/packages/opencode/src/acp/service.ts b/packages/opencode/src/acp/service.ts index 55fbc9681d..1ec185939d 100644 --- a/packages/opencode/src/acp/service.ts +++ b/packages/opencode/src/acp/service.ts @@ -30,6 +30,7 @@ import { type SetSessionModeResponse, } from "@agentclientprotocol/sdk" import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import type { AssistantMessage, Message, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" import { Context, Effect, Layer, ManagedRuntime } from "effect" @@ -94,17 +95,17 @@ export function make(input: { const initialize = Effect.fn("ACP.initialize")(function* (params: InitializeRequest) { const started = performance.now() const authMethod: AuthMethod = { - description: "Run `opencode auth login` in the terminal", - name: "Login with opencode", + description: `Run \`${OpenCodezIdentity.cliName} auth login\` in the terminal`, + name: `Login with ${OpenCodezIdentity.productName}`, id: AuthMethodID, } if (params.clientCapabilities?._meta?.["terminal-auth"] === true) { authMethod._meta = { "terminal-auth": { - command: "opencode", + command: OpenCodezIdentity.cliName, args: ["auth", "login"], - label: "OpenCode Login", + label: `${OpenCodezIdentity.productName} Login`, }, } } @@ -130,7 +131,7 @@ export function make(input: { }, authMethods: [authMethod], agentInfo: { - name: "OpenCode", + name: OpenCodezIdentity.productName, version: InstallationVersion, }, } @@ -874,7 +875,7 @@ const promptResponse = Effect.fn("ACP.promptResponse")(function* ( function promptErrorMessage(error: AssistantError) { if ("message" in error.data && typeof error.data.message === "string") return error.data.message - return "OpenCode prompt failed" + return `${OpenCodezIdentity.productName} prompt failed` } function sendUsageUpdate( @@ -1067,7 +1068,7 @@ function fromUnknownError(error: unknown, service?: string): Error { if (isAuthRequired(error)) { return new ACPError.AuthRequiredError({ providerId: findProviderID(error) }) } - return new ACPError.ServiceFailureError({ safeMessage: "OpenCode service failure", service }) + return new ACPError.ServiceFailureError({ safeMessage: `${OpenCodezIdentity.productName} service failure`, service }) } function isACPError(error: unknown): error is Error { diff --git a/packages/opencode/src/cli/cmd/account.ts b/packages/opencode/src/cli/cmd/account.ts index b9cbf5569c..f439c18731 100644 --- a/packages/opencode/src/cli/cmd/account.ts +++ b/packages/opencode/src/cli/cmd/account.ts @@ -15,7 +15,7 @@ const dim = (value: string) => UI.Style.TEXT_DIM + value + UI.Style.TEXT_NORMAL const activeSuffix = (isActive: boolean) => (isActive ? dim(" (active)") : "") -export const defaultConsoleUrl = "https://console.opencode.ai" +export const defaultConsoleUrl = "https://opencode.ai/console" export const formatAccountLabel = (account: { email: string; url: string }, isActive: boolean) => `${account.email} ${dim(account.url)}${activeSuffix(isActive)}` diff --git a/packages/opencode/src/cli/cmd/attach.ts b/packages/opencode/src/cli/cmd/attach.ts index 6f5aea6a12..e8836b68c0 100644 --- a/packages/opencode/src/cli/cmd/attach.ts +++ b/packages/opencode/src/cli/cmd/attach.ts @@ -3,10 +3,11 @@ import { UI } from "@/cli/ui" import { errorMessage } from "@opencode-ai/tui/util/error" import { validateSession } from "../tui/validate-session" import { ServerAuth } from "@/server/auth" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" export const AttachCommand = cmd({ command: "attach ", - describe: "attach to a running opencode server", + describe: `attach to a running ${OpenCodezIdentity.productName} server`, builder: (yargs) => yargs .positional("url", { diff --git a/packages/opencode/src/cli/cmd/debug/index.ts b/packages/opencode/src/cli/cmd/debug/index.ts index 9dcaa33b36..297bca753a 100644 --- a/packages/opencode/src/cli/cmd/debug/index.ts +++ b/packages/opencode/src/cli/cmd/debug/index.ts @@ -1,6 +1,7 @@ import { Global } from "@opencode-ai/core/global" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { Flag } from "@opencode-ai/core/flag/flag" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" import os from "os" import { Duration, Effect } from "effect" import { effectCmd } from "../../effect-cmd" @@ -58,7 +59,7 @@ const InfoCommand = effectCmd({ : undefined const terminal = [termProgram, process.env.TERM].filter((item): item is string => Boolean(item)).join(" / ") - console.log(`opencode version: ${InstallationVersion}`) + console.log(`${OpenCodezIdentity.productName} version: ${InstallationVersion}`) console.log(`os: ${os.type()} ${os.release()} ${os.arch()}`) console.log(`terminal: ${terminal || "unknown"}`) console.log("plugins:") diff --git a/packages/opencode/src/cli/cmd/github.handler.ts b/packages/opencode/src/cli/cmd/github.handler.ts index 6511ab30ea..fcf44279ce 100644 --- a/packages/opencode/src/cli/cmd/github.handler.ts +++ b/packages/opencode/src/cli/cmd/github.handler.ts @@ -437,6 +437,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: let session: { id: SessionID; title: string; version: string } let shareId: string | undefined let exitCode = 0 + let githubClientReady = false type PromptFiles = Awaited>["promptFiles"] const triggerCommentId = isCommentEvent ? (payload as IssueCommentEvent | PullRequestReviewCommentEvent).comment.id @@ -485,6 +486,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: octoGraph = graphql.defaults({ headers: { authorization: `token ${appToken}` }, }) + githubClientReady = true const { userPrompt, promptFiles } = await getUserPrompt() if (!useGithubToken) { @@ -639,9 +641,13 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: } else if (e instanceof Error) { msg = e.message } - if (isUserEvent) { - await createComment(`${msg}${footer()}`) - await removeReaction(commentType) + if (isUserEvent && githubClientReady) { + try { + await createComment(`${msg}${footer()}`) + await removeReaction(commentType) + } catch (error) { + console.error("Failed to report error on GitHub:", error) + } } core.setFailed(msg) // Also output the clean error message for the action to capture @@ -1004,8 +1010,9 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: }) if (!response.ok) { - const responseJson = (await response.json()) as { error?: string } - throw new Error(`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`) + throw new Error( + `App token exchange failed: ${response.status} ${response.statusText} - ${await response.text()}`, + ) } const responseJson = (await response.json()) as { token: string } diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index c2d2ee2f3b..4bb9d6d9ea 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -20,6 +20,7 @@ import { Global } from "@opencode-ai/core/global" import { modify, applyEdits } from "jsonc-parser" import { Filesystem } from "@/util/filesystem" import { Effect } from "effect" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" function getAuthStatusIcon(status: MCP.AuthStatus): string { switch (status) { @@ -119,7 +120,7 @@ export const McpListCommand = effectCmd({ if (servers.length === 0) { prompts.log.warn("No MCP servers configured") - prompts.outro("Add servers with: opencode mcp add") + prompts.outro(`Add servers with: ${OpenCodezIdentity.cliName} mcp add`) return } diff --git a/packages/opencode/src/cli/cmd/pr.ts b/packages/opencode/src/cli/cmd/pr.ts index 4209722357..5258ed910d 100644 --- a/packages/opencode/src/cli/cmd/pr.ts +++ b/packages/opencode/src/cli/cmd/pr.ts @@ -4,10 +4,11 @@ import { effectCmd, fail } from "../effect-cmd" import { Git } from "@/git" import { InstanceRef } from "@/effect/instance-ref" import { Process } from "@/util/process" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" export const PrCommand = effectCmd({ command: "pr ", - describe: "fetch and checkout a GitHub PR branch, then run opencode", + describe: `fetch and checkout a GitHub PR branch, then run ${OpenCodezIdentity.productName}`, builder: (yargs) => yargs.positional("number", { type: "number", @@ -76,11 +77,11 @@ export const PrCommand = effectCmd({ const sessionMatch = prInfo.body.match(/https:\/\/opncd\.ai\/s\/([a-zA-Z0-9_-]+)/) if (sessionMatch) { const sessionUrl = sessionMatch[0] - UI.println(`Found opencode session: ${sessionUrl}`) + UI.println(`Found ${OpenCodezIdentity.productName} session: ${sessionUrl}`) UI.println(`Importing session...`) const importResult = yield* Effect.promise(() => - Process.text(["opencode", "import", sessionUrl], { nothrow: true }), + Process.text([OpenCodezIdentity.cliName, "import", sessionUrl], { nothrow: true }), ) if (importResult.code === 0) { const sessionIdMatch = importResult.text.trim().match(/Imported session: ([a-zA-Z0-9_-]+)/) @@ -95,13 +96,13 @@ export const PrCommand = effectCmd({ UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`) UI.println() - UI.println("Starting opencode...") + UI.println(`Starting ${OpenCodezIdentity.productName}...`) UI.println() const opencodeArgs = sessionId ? ["-s", sessionId] : [] const code = yield* Effect.promise( () => - Process.spawn(["opencode", ...opencodeArgs], { + Process.spawn([OpenCodezIdentity.cliName, ...opencodeArgs], { stdin: "inherit", stdout: "inherit", stderr: "inherit", @@ -110,6 +111,6 @@ export const PrCommand = effectCmd({ ) // Match legacy throw semantics — propagate as a defect so the top-level // index.ts catch handles it identically (exit 1, "Unexpected error" banner). - if (code !== 0) return yield* Effect.die(new Error(`opencode exited with code ${code}`)) + if (code !== 0) return yield* Effect.die(new Error(`${OpenCodezIdentity.cliName} exited with code ${code}`)) }), }) diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 3775123d83..579d2792b5 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -17,6 +17,7 @@ import { Process } from "@/util/process" import { errorMessage } from "@/util/error" import { text } from "node:stream/consumers" import { Effect, Option } from "effect" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" type PluginAuth = NonNullable @@ -304,7 +305,7 @@ export const ProvidersLoginCommand = effectCmd({ builder: (yargs: Argv) => yargs .positional("url", { - describe: "opencode auth provider", + describe: `${OpenCodezIdentity.cliName} auth provider`, type: "string", }) .option("provider", { diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 3927f615a0..10185207c4 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -1,5 +1,6 @@ import type { PermissionV1 } from "@opencode-ai/core/v1/permission" import { FSUtil } from "@opencode-ai/core/fs-util" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" // CLI entry point for `opencode run` and `opencode --mini`. // // Handles three modes: @@ -125,7 +126,7 @@ async function toolError(part: ToolPart) { export const RunCommand = effectCmd({ command: "run [message..]", - describe: "run opencode with a message", + describe: `run ${OpenCodezIdentity.productName} with a message`, // --attach connects to a remote server (no local instance needed); the // default path runs an in-process server and needs the project instance. instance: (args) => !args.attach, @@ -189,7 +190,7 @@ export const RunCommand = effectCmd({ }) .option("attach", { type: "string", - describe: "attach to a running opencode server (e.g., http://localhost:4096)", + describe: `attach to a running ${OpenCodezIdentity.productName} server (e.g., http://localhost:4096)`, }) .option("password", { alias: ["p"], @@ -696,9 +697,14 @@ export const RunCommand = effectCmd({ // created, and replies issued from inside the loop must use that client. async function loop(client: OpencodeClient, events: Awaited>) { const toggles = new Map() + const sessions = new Set([sessionID]) let error: string | undefined for await (const event of events.stream) { + if (event.type === "session.created" && event.properties.info.parentID) { + if (sessions.has(event.properties.info.parentID)) sessions.add(event.properties.info.id) + } + if ( event.type === "message.updated" && event.properties.sessionID === sessionID && @@ -795,7 +801,7 @@ export const RunCommand = effectCmd({ if (event.type === "permission.asked") { const permission = event.properties - if (permission.sessionID !== sessionID) continue + if (!sessions.has(permission.sessionID)) continue if (auto) { await client.permission.reply({ diff --git a/packages/opencode/src/cli/cmd/run/splash.ts b/packages/opencode/src/cli/cmd/run/splash.ts index 141ff6fc55..4163dd116c 100644 --- a/packages/opencode/src/cli/cmd/run/splash.ts +++ b/packages/opencode/src/cli/cmd/run/splash.ts @@ -19,6 +19,7 @@ import { } from "@opentui/core" import * as Locale from "@/util/locale" import { go } from "@/cli/logo" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" import type { RunSplashTheme } from "./theme" export const SPLASH_TITLE_LIMIT = 50 @@ -234,7 +235,7 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback lines, body_left + label.length, top + 1, - `opencode --mini -s ${meta.session_id}`, + `${OpenCodezIdentity.cliName} --mini -s ${meta.session_id}`, right, undefined, TextAttributes.BOLD, diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index c0f62b3ca0..af95e31eaa 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -2,11 +2,12 @@ import { Effect } from "effect" import { effectCmd } from "../effect-cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" import { Flag } from "@opencode-ai/core/flag/flag" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" export const ServeCommand = effectCmd({ command: "serve", builder: (yargs) => withNetworkOptions(yargs), - describe: "starts a headless opencode server", + describe: `starts a headless ${OpenCodezIdentity.productName} server`, // Server loads instances per-request via x-opencode-directory header — no // need for an ambient project InstanceContext at startup. instance: false, @@ -17,6 +18,7 @@ export const ServeCommand = effectCmd({ } const opts = yield* resolveNetworkOptions(args) const server = yield* Effect.promise(() => Server.listen(opts)) + // This line is also the SDK subprocess readiness protocol. console.log(`opencode server listening on http://${server.hostname}:${server.port}`) yield* Effect.never diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index 95ffac7ea5..de88da3485 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -14,6 +14,7 @@ import { writeHeapSnapshot } from "v8" import { ServerAuth } from "@/server/auth" import { validateSession } from "../tui/validate-session" import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" declare global { const OPENCODE_WORKER_PATH: string @@ -71,12 +72,12 @@ export function resolveThreadDirectory(project?: string, envPWD = process.env.PW export const TuiThreadCommand = cmd({ command: "$0 [project]", - describe: "start opencode tui", + describe: `start ${OpenCodezIdentity.productName} TUI`, builder: (yargs) => withNetworkOptions(yargs) .positional("project", { type: "string", - describe: "path to start opencode in", + describe: `path to start ${OpenCodezIdentity.productName} in`, }) .option("model", { type: "string", diff --git a/packages/opencode/src/cli/cmd/upgrade.ts b/packages/opencode/src/cli/cmd/upgrade.ts index 3c1604a0b8..22b0a817ac 100644 --- a/packages/opencode/src/cli/cmd/upgrade.ts +++ b/packages/opencode/src/cli/cmd/upgrade.ts @@ -3,10 +3,11 @@ import { UI } from "../ui" import * as prompts from "@clack/prompts" import { Installation } from "../../installation" import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" export const UpgradeCommand = { command: "upgrade [target]", - describe: "upgrade opencode to the latest or a specific version", + describe: `upgrade ${OpenCodezIdentity.cliName} to the latest or a specific version`, builder: (yargs: Argv) => { return yargs .positional("target", { @@ -28,7 +29,9 @@ export const UpgradeCommand = { const detectedMethod = await Installation.method() const method = (args.method as Installation.Method) ?? detectedMethod if (method === "unknown") { - prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`) + prompts.log.error( + `${OpenCodezIdentity.cliName} is installed to ${process.execPath} and may be managed by a package manager`, + ) const install = await prompts.select({ message: "Install anyways?", options: [ @@ -46,7 +49,7 @@ export const UpgradeCommand = { const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest() if (InstallationVersion === target) { - prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`) + prompts.log.warn(`${OpenCodezIdentity.cliName} upgrade skipped: ${target} is already installed`) prompts.outro("Done") return } diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 69a981aada..66f85d3d31 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -3,6 +3,7 @@ import { UI } from "../ui" import { effectCmd } from "../effect-cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" import { Flag } from "@opencode-ai/core/flag/flag" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" import open from "open" import { networkInterfaces } from "os" @@ -31,7 +32,7 @@ function getNetworkIPs() { export const WebCommand = effectCmd({ command: "web", builder: (yargs) => withNetworkOptions(yargs), - describe: "start opencode server and open web interface", + describe: `start ${OpenCodezIdentity.productName} server and open web interface`, // Server loads instances per-request via x-opencode-directory header — no // ambient project InstanceContext needed at startup. instance: false, diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 407547e4e5..8ac1eb8a25 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -1,6 +1,7 @@ import { NamedError } from "@opencode-ai/core/util/error" import { errorFormat } from "@/util/error" import { isRecord } from "@/util/record" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" type ConfigIssue = { message: string; path: string[] } @@ -47,7 +48,7 @@ export function FormatError(input: unknown): string | undefined { // MCPFailed: { name: string } if (NamedError.hasName(input, "MCPFailed")) { const data = isRecord(input) && isRecord(input.data) ? stringField(input.data, "name") : undefined - return `MCP server "${data}" failed. Note, opencode does not support MCP authentication yet.` + return `MCP server "${data}" failed. Note, ${OpenCodezIdentity.productName} does not support MCP authentication yet.` } // AccountServiceError, AccountTransportError: TaggedErrorClass @@ -64,7 +65,7 @@ export function FormatError(input: unknown): string | undefined { return [ `Model not found: ${stringField(providerModelNotFound, "providerID")}/${stringField(providerModelNotFound, "modelID")}`, ...(suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []), - `Try: \`opencode models\` to list available models`, + `Try: \`${OpenCodezIdentity.cliName} models\` to list available models`, `Or check your config (opencode.json) provider/model names`, ].join("\n") } @@ -102,7 +103,7 @@ export function FormatError(input: unknown): string | undefined { return [ `Failed to load remote config${remote ? ` from ${remote}` : ""}: the server returned a login page instead of JSON.`, `Authentication is missing or has expired (the endpoint is likely behind an SSO or identity-aware proxy).`, - ...(url ? [`Run \`opencode auth login ${url}\` to re-authenticate.`] : []), + ...(url ? [`Run \`${OpenCodezIdentity.cliName} auth login ${url}\` to re-authenticate.`] : []), ].join("\n") } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 050b650787..4f7f18ae98 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -33,6 +33,7 @@ import { ConfigParse } from "./parse" import { ConfigPaths } from "./paths" import { ConfigPlugin } from "./plugin" import { ConfigVariable } from "./variable" +import { ConfigV2Compat } from "./v2-compat" import { Npm } from "@opencode-ai/core/npm" import { withTransientReadRetry } from "@/util/effect-http-client" @@ -184,6 +185,19 @@ const layer = Layer.effect( const readConfigFile = (filepath: string) => fs.readFileStringSafe(filepath).pipe(Effect.orDie) + const decodeConfig = Effect.fnUntraced(function* (input: unknown, source: string) { + const result = ConfigV2Compat.lower(normalizeLoadedConfig(input), source) + yield* Effect.forEach(result.diagnostics, (diagnostic) => + Effect.logWarning("configuration compatibility diagnostic", { + source, + path: diagnostic.path, + kind: diagnostic.kind, + action: diagnostic.message, + }), + ) + return ConfigParse.schema(ConfigV1.Info, result.value, source) + }) + const fetchRemoteJson = Effect.fnUntraced(function* ( url: string, headers: Record | undefined, @@ -224,7 +238,7 @@ const layer = Layer.effect( ), ) const parsed = ConfigParse.jsonc(expanded, source) - const data = ConfigParse.schema(ConfigV1.Info, normalizeLoadedConfig(parsed), source) + const data = yield* decodeConfig(parsed, source) if (!("path" in options)) return data yield* Effect.promise(() => resolveLoadedPlugins(data, options.path)) @@ -625,8 +639,13 @@ const layer = Layer.effect( const dir = yield* InstanceState.directory const file = path.join(dir, "config.json") const existing = yield* loadFile(file) + const text = yield* readConfigFile(file) + const original = text ? ConfigParse.jsonc(text, file) : writable(existing) yield* fs - .writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2)) + .writeFileString( + file, + JSON.stringify(mergeDeep(isRecord(original) ? original : writable(existing), writable(config)), null, 2), + ) .pipe(Effect.orDie) }) @@ -642,15 +661,16 @@ const layer = Layer.effect( let next: Info let changed: boolean if (!file.endsWith(".jsonc")) { - const existing = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(before, file), file) - const merged = mergeDeep(writable(existing), patch) + const existing = ConfigParse.jsonc(before, file) + ConfigParse.schema(ConfigV1.Info, ConfigV2Compat.lower(normalizeLoadedConfig(existing), file).value, file) + const merged = mergeDeep(isRecord(existing) ? existing : {}, patch) const serialized = JSON.stringify(merged, null, 2) + next = yield* decodeConfig(merged, file) changed = serialized !== before if (changed) yield* fs.writeFileString(file, serialized).pipe(Effect.orDie) - next = merged } else { const updated = patchJsonc(before, patch) - next = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(updated, file), file) + next = yield* decodeConfig(ConfigParse.jsonc(updated, file), file) changed = updated !== before if (changed) yield* fs.writeFileString(file, updated).pipe(Effect.orDie) } diff --git a/packages/opencode/src/config/v2-compat.ts b/packages/opencode/src/config/v2-compat.ts new file mode 100644 index 0000000000..9e4e0bf540 --- /dev/null +++ b/packages/opencode/src/config/v2-compat.ts @@ -0,0 +1,449 @@ +export * as ConfigV2Compat from "./v2-compat" + +import { isDeepStrictEqual } from "node:util" +import { Option, Schema } from "effect" +import { NonNegativeInt, PositiveInt } from "@opencode-ai/core/schema" +import { ConfigAttachmentV1 } from "@opencode-ai/core/v1/config/attachment" +import { ConfigLSPV1 } from "@opencode-ai/core/v1/config/lsp" +import { InvalidError } from "@opencode-ai/core/v1/config/error" + +export interface Diagnostic { + readonly kind: "invalid" | "unsupported" | "conflict" + readonly path: readonly string[] + readonly message: string +} + +export interface Result { + readonly value: unknown + readonly diagnostics: readonly Diagnostic[] +} + +const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const +const Record = Schema.Record(Schema.String, Schema.Unknown) +const Timeout = Schema.Struct({ + startup: Schema.optional(PositiveInt), + catalog: Schema.optional(PositiveInt), + execution: Schema.optional(PositiveInt), +}) +const OAuth = Schema.Struct({ + client_id: Schema.optional(Schema.String), + client_secret: Schema.optional(Schema.String), + scope: Schema.optional(Schema.String), + callback_port: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))), + redirect_uri: Schema.optional(Schema.String), +}) +const Server = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("local"), + command: Schema.Array(Schema.String), + cwd: Schema.optional(Schema.String), + environment: Schema.optional(Schema.Record(Schema.String, Schema.String)), + disabled: Schema.optional(Schema.Boolean), + codemode: Schema.optional(Schema.Boolean), + timeout: Schema.optional(Timeout), + }), + Schema.Struct({ + type: Schema.Literal("remote"), + url: Schema.String, + headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), + oauth: Schema.optional(Schema.Union([OAuth, Schema.Literal(false)])), + disabled: Schema.optional(Schema.Boolean), + codemode: Schema.optional(Schema.Boolean), + timeout: Schema.optional(Timeout), + }), +]) +const Selection = Schema.Union([ + Schema.String.check(Schema.isPattern(/^[^/#]+\/[^#]+(?:#[^#]+)?$/)), + Schema.Struct({ + providerID: Schema.String.check(Schema.isPattern(/^[^/#]+$/)), + model: Schema.String.check(Schema.isPattern(/^[^#]+$/)), + variant: Schema.optional(Schema.String.check(Schema.isPattern(/^[^#]+$/))), + }), +]) +const Agent = Schema.Struct({ + model: Schema.optional(Selection), + request: Schema.optional( + Schema.Struct({ + headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), + body: Schema.optional(Schema.Record(Schema.String, Schema.Json)), + }), + ), + system: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])), + hidden: Schema.optional(Schema.Boolean), + color: Schema.optional(Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))), + steps: Schema.optional(PositiveInt), + disabled: Schema.optional(Schema.Boolean), +}) +const Command = Schema.Struct({ + template: Schema.String, + description: Schema.optional(Schema.String), + agent: Schema.optional(Schema.String), + model: Schema.optional(Selection), + subtask: Schema.optional(Schema.Boolean), +}) + +const decodeRecord = Schema.decodeUnknownOption(Record, decodeOptions) +const decodeLspEntry = Schema.decodeUnknownOption(ConfigLSPV1.Entry, decodeOptions) +const builtinServers = new Set(ConfigLSPV1.builtinServerIds) + +export function lower(input: unknown, source = "configuration"): Result { + const parsed = decodeRecord(input) + if (Option.isNone(parsed)) return { value: input, diagnostics: [] } + + const permissions = [ + ...(Object.hasOwn(parsed.value, "permissions") ? [["permissions"]] : []), + ...["agents", "agent", "mode"].flatMap((key) => { + const agents = decodeRecord(parsed.value[key]) + if (Option.isNone(agents)) return [] + return Object.entries(agents.value).flatMap(([name, value]) => { + const agent = decodeRecord(value) + return Option.isSome(agent) && Object.hasOwn(agent.value, "permissions") ? [[key, name, "permissions"]] : [] + }) + }), + ] + if (permissions.length) + throw new InvalidError({ + path: source, + issues: permissions.map((path) => ({ + path, + message: 'V2 permissions are not supported by OpenCode V1. Use V1 "permission" rules or run opencode2.', + })), + }) + + const result: Record = { ...parsed.value } + const diagnostics: Diagnostic[] = [] + for (const key of ["plugins", "providers", "websearch", "warming"]) + if (Object.hasOwn(parsed.value, key)) unsupported([key], diagnostics) + + normalizeSettings(parsed.value, result, diagnostics) + normalizeModel(parsed.value, result, diagnostics) + normalizeSkills(parsed.value, result, diagnostics) + normalizeCompaction(parsed.value, result, diagnostics) + normalizeExperimental(parsed.value, result, diagnostics) + + normalizeAgents(parsed.value, result, diagnostics) + normalizeCommands(parsed.value, result, diagnostics) + normalizeMcp(parsed.value, result, diagnostics) + normalizeLsp(parsed.value, result, diagnostics) + + return { value: result, diagnostics } +} + +function normalizeSettings(input: Record, result: Record, diagnostics: Diagnostic[]) { + if (Object.hasOwn(input, "snapshots")) { + const value = decodeValue(Schema.Boolean, input.snapshots, ["snapshots"], diagnostics) + if (value !== undefined) preferLegacy(result, "snapshot", value, ["snapshots"], diagnostics) + } + if (Object.hasOwn(input, "media")) { + const value = decodeValue(ConfigAttachmentV1.Info, input.media, ["media"], diagnostics) + if (value !== undefined) preferLegacy(result, "attachment", value, ["media"], diagnostics) + } +} + +function normalizeModel(input: Record, result: Record, diagnostics: Diagnostic[]) { + if (!Object.hasOwn(input, "model")) return + const selection = Schema.decodeUnknownOption(Selection, decodeOptions)(input.model) + if (Option.isNone(selection)) return + const value = lowerSelection(selection.value) + result.model = value.model + if (value.variant !== undefined) unsupported(["model", "variant"], diagnostics) +} + +function normalizeSkills(input: Record, result: Record, diagnostics: Diagnostic[]) { + if (!Array.isArray(input.skills)) return + const skills = decodeValue(Schema.Array(Schema.String), input.skills, ["skills"], diagnostics) + if (skills === undefined) return + result.skills = { + paths: skills.filter((value) => !/^https?:\/\//i.test(value)), + urls: skills.filter((value) => /^https?:\/\//i.test(value)), + } +} + +function normalizeCompaction( + input: Record, + result: Record, + diagnostics: Diagnostic[], +) { + const compaction = decodeRecord(input.compaction) + if (Option.isNone(compaction)) return + const value = { ...compaction.value } + if (Object.hasOwn(value, "keep")) { + const keep = decodeValue(Record, value.keep, ["compaction", "keep"], diagnostics) + if (keep !== undefined && Object.hasOwn(keep, "tokens")) { + const tokens = decodeValue(NonNegativeInt, keep.tokens, ["compaction", "keep", "tokens"], diagnostics) + if (tokens !== undefined) + preferLegacy(value, "preserve_recent_tokens", tokens, ["compaction", "keep", "tokens"], diagnostics) + } + } + if (Object.hasOwn(value, "buffer")) { + const buffer = decodeValue(NonNegativeInt, value.buffer, ["compaction", "buffer"], diagnostics) + if (buffer !== undefined) preferLegacy(value, "reserved", buffer, ["compaction", "buffer"], diagnostics) + } + result.compaction = value +} + +function normalizeExperimental( + input: Record, + result: Record, + diagnostics: Diagnostic[], +) { + const experimental = decodeRecord(input.experimental) + if (Option.isNone(experimental)) return + if (Object.hasOwn(experimental.value, "portable_shell_scanner")) + unsupported(["experimental", "portable_shell_scanner"], diagnostics) + if (!Object.hasOwn(experimental.value, "subagent_depth")) return + const depth = decodeValue( + NonNegativeInt, + experimental.value.subagent_depth, + ["experimental", "subagent_depth"], + diagnostics, + ) + if (depth !== undefined) + preferLegacy(result, "subagent_depth", depth, ["experimental", "subagent_depth"], diagnostics) +} + +function normalizeAgents(input: Record, result: Record, diagnostics: Diagnostic[]) { + if (!Object.hasOwn(input, "agents")) return + const agents = decodeValue(Record, input.agents, ["agents"], diagnostics) + if (agents === undefined) return + const legacy = decodeRecord(result.agent) + const merged: Record = Option.isSome(legacy) ? { ...legacy.value } : {} + for (const [name, value] of Object.entries(agents)) { + const path = ["agents", name] + if (Object.hasOwn(merged, name)) { + if (!isDeepStrictEqual(merged[name], value)) conflict(path, diagnostics) + continue + } + const parsed = decodeValue(Agent, value, path, diagnostics) + if (parsed === undefined) continue + if (parsed.request?.headers !== undefined) unsupported([...path, "request", "headers"], diagnostics) + setOwn(merged, name, lowerAgent(parsed)) + } + if (Object.hasOwn(result, "agent") && Option.isNone(legacy)) return + if (Object.keys(merged).length > 0 || Option.isSome(legacy)) result.agent = merged +} + +function normalizeCommands(input: Record, result: Record, diagnostics: Diagnostic[]) { + if (!Object.hasOwn(input, "commands")) return + const commands = decodeValue(Record, input.commands, ["commands"], diagnostics) + if (commands === undefined) return + const legacy = decodeRecord(result.command) + if (Object.hasOwn(result, "command") && Option.isNone(legacy)) return + const merged: Record = Option.isSome(legacy) ? { ...legacy.value } : {} + for (const [name, value] of Object.entries(commands)) { + const path = ["commands", name] + const parsed = decodeValue(Command, value, path, diagnostics) + if (parsed === undefined) continue + preferLegacy(merged, name, lowerCommand(parsed), path, diagnostics) + } + if (Object.keys(merged).length > 0 || Option.isSome(legacy)) result.command = merged +} + +function normalizeMcp(input: Record, result: Record, diagnostics: Diagnostic[]) { + const mcp = decodeRecord(input.mcp) + if (Option.isNone(mcp)) return + const servers: Record = {} + const nested = decodeRecord(mcp.value.servers) + const envelope = Option.isSome(nested) && !isDirectServer(nested.value) + const timeoutRecord = decodeRecord(mcp.value.timeout) + const timeout = Schema.decodeUnknownOption(Timeout, decodeOptions)(mcp.value.timeout) + const globalTimeout = + Option.isSome(timeout) && + Option.isSome(timeoutRecord) && + !isDirectServer(timeoutRecord.value) && + (Object.keys(timeoutRecord.value).length === 0 || + ["startup", "catalog", "execution"].some((key) => Object.hasOwn(timeoutRecord.value, key))) + + for (const [name, value] of Object.entries(mcp.value)) { + if (name === "servers" && envelope) continue + if (name === "timeout" && globalTimeout) continue + const path = ["mcp", name] + const record = decodeRecord(value) + const oauth = Option.isSome(record) ? decodeRecord(record.value.oauth) : Option.none() + const native = + Option.isSome(record) && + (Object.hasOwn(record.value, "disabled") || + Object.hasOwn(record.value, "codemode") || + typeof record.value.timeout === "object" || + (Option.isSome(oauth) && + ["client_id", "client_secret", "callback_port", "redirect_uri"].some((key) => + Object.hasOwn(oauth.value, key), + ))) + // Keep invalid flat entries for the final V1 decoder rather than sanitizing them. + setOwn(servers, name, native ? (normalizeServer(value, path, diagnostics) ?? value) : value) + } + + if (envelope && Option.isSome(nested)) { + for (const [name, value] of Object.entries(nested.value)) { + const path = ["mcp", "servers", name] + if (Object.hasOwn(servers, name)) { + if (!isDeepStrictEqual(servers[name], value)) conflict(path, diagnostics) + continue + } + const record = decodeRecord(value) + if ( + Option.isSome(record) && + typeof record.value.enabled === "boolean" && + !Object.hasOwn(record.value, "disabled") + ) { + setOwn(servers, name, value) + continue + } + const server = normalizeServer(value, path, diagnostics) + if (server !== undefined) setOwn(servers, name, server) + } + } + result.mcp = servers + + if (!globalTimeout || Option.isNone(timeout)) return + const value = lowerTimeout(timeout.value) + if (value === undefined) { + if (Object.keys(timeout.value).length) unsupported(["mcp", "timeout"], diagnostics) + return + } + const existing = decodeRecord(result.experimental) + if (Object.hasOwn(result, "experimental") && Option.isNone(existing)) return + const experimental = Option.isSome(existing) ? { ...existing.value } : {} + preferLegacy(experimental, "mcp_timeout", value, ["mcp", "timeout"], diagnostics) + result.experimental = experimental +} + +function isDirectServer(value: Record) { + // Object-valued entries can be servers literally named "type" or "enabled". + return ["type", "enabled"].some( + (key) => + Object.hasOwn(value, key) && (value[key] === null || typeof value[key] !== "object" || Array.isArray(value[key])), + ) +} + +function normalizeServer(input: unknown, path: string[], diagnostics: Diagnostic[]) { + const server = decodeValue(Server, input, path, diagnostics) + if (server === undefined) return + if (server.codemode !== undefined) unsupported([...path, "codemode"], diagnostics) + if (server.timeout && lowerTimeout(server.timeout) === undefined && Object.keys(server.timeout).length) + unsupported([...path, "timeout"], diagnostics) + const raw = decodeRecord(input) + if (Option.isNone(raw) || !Object.hasOwn(raw.value, "enabled")) return lowerServer(server) + if (server.disabled !== undefined && raw.value.enabled === server.disabled) + conflict([...path, "disabled"], diagnostics) + return { ...lowerServer(server), enabled: raw.value.enabled } +} + +function normalizeLsp(input: Record, result: Record, diagnostics: Diagnostic[]) { + const lsp = decodeRecord(input.lsp) + if (Option.isNone(lsp)) return + result.lsp = Object.fromEntries( + Object.entries(lsp.value).filter(([name, value]) => { + if (builtinServers.has(name)) return true + const entry = decodeLspEntry(value) + if (Option.isNone(entry)) return true + if (entry.value.disabled === true) return true + if ("extensions" in entry.value && entry.value.extensions !== undefined) return true + unsupported(["lsp", name], diagnostics) + return false + }), + ) +} + +function lowerSelection(input: Schema.Schema.Type) { + if (typeof input !== "string") { + return { + model: `${input.providerID}/${input.model}`, + ...(input.variant !== undefined ? { variant: input.variant } : {}), + } + } + const index = input.indexOf("#") + if (index === -1) return { model: input } + return { model: input.slice(0, index), variant: input.slice(index + 1) } +} + +function lowerTimeout(input: Schema.Schema.Type) { + if (input.startup !== undefined) return undefined + if (input.catalog === undefined || input.execution === undefined) return undefined + if (input.catalog !== input.execution) return undefined + return input.catalog +} + +function lowerServer(input: Schema.Schema.Type) { + const result: Record = { + ...input, + enabled: input.disabled !== true, + } + delete result.disabled + delete result.codemode + delete result.timeout + + if (input.timeout) { + const timeout = lowerTimeout(input.timeout) + if (timeout !== undefined) result.timeout = timeout + } + + if (input.type === "remote" && input.oauth && typeof input.oauth === "object") { + const oauth: Record = {} + if (input.oauth.client_id !== undefined) oauth.clientId = input.oauth.client_id + if (input.oauth.client_secret !== undefined) oauth.clientSecret = input.oauth.client_secret + if (input.oauth.scope !== undefined) oauth.scope = input.oauth.scope + if (input.oauth.callback_port !== undefined) oauth.callbackPort = input.oauth.callback_port + if (input.oauth.redirect_uri !== undefined) oauth.redirectUri = input.oauth.redirect_uri + result.oauth = oauth + } + + return result +} + +function lowerAgent(input: Schema.Schema.Type) { + const result: Record = {} + for (const key of ["description", "mode", "hidden", "color", "steps"] as const) { + if (input[key] !== undefined) result[key] = input[key] + } + if (input.system !== undefined) result.prompt = input.system + if (input.disabled !== undefined) result.disable = input.disabled + if (input.model !== undefined) Object.assign(result, lowerSelection(input.model)) + if (input.request?.body !== undefined) result.options = input.request.body + + return result +} + +function lowerCommand(input: Schema.Schema.Type) { + return { ...input, ...(input.model !== undefined ? lowerSelection(input.model) : {}) } +} + +function decodeValue>( + schema: S, + value: unknown, + path: string[], + diagnostics: Diagnostic[], +) { + const decoded = Schema.decodeUnknownOption(schema, decodeOptions)(value) + if (Option.isSome(decoded)) return decoded.value + diagnostics.push({ kind: "invalid", path, message: "Native setting could not be lowered because it is malformed" }) + return undefined +} + +function preferLegacy( + target: Record, + key: string, + value: unknown, + path: string[], + diagnostics: Diagnostic[], +) { + if (Object.hasOwn(target, key)) { + if (!isDeepStrictEqual(target[key], value)) conflict(path, diagnostics) + return + } + setOwn(target, key, value) +} + +function setOwn(target: Record, key: string, value: unknown) { + Object.defineProperty(target, key, { value, enumerable: true, configurable: true, writable: true }) +} + +function unsupported(path: string[], diagnostics: Diagnostic[]) { + diagnostics.push({ kind: "unsupported", path, message: "Omitted native setting that cannot be represented in V1" }) +} + +function conflict(path: string[], diagnostics: Diagnostic[]) { + diagnostics.push({ kind: "conflict", path, message: "Retained legacy value over native value" }) +} diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 8f746e2568..188cd383bb 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -714,6 +714,7 @@ const layer = Layer.effect( }) const list = Effect.fn("Workspace.list")(function* (project: Project.Info) { + if (!flags.experimentalWorkspaces) return [] return (yield* db .select() .from(WorkspaceTable) @@ -851,6 +852,7 @@ const layer = Layer.effect( }) const startWorkspaceSyncing = Effect.fn("Workspace.startWorkspaceSyncing")(function* (projectID: ProjectV2.ID) { + if (!flags.experimentalWorkspaces) return const rows = yield* db .selectDistinct({ workspace: WorkspaceTable }) .from(WorkspaceTable) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 05f12fa2ee..ac9c8f55b7 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -33,6 +33,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { McpCatalog } from "./catalog" import { McpEvent } from "@opencode-ai/schema/mcp-event" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" import { McpBrowser } from "./browser" const DEFAULT_TIMEOUT = 30_000 @@ -314,7 +315,7 @@ const layer = Layer.effect( return events .publish(TuiEvent.ToastShow, { title: "MCP Authentication Required", - message: `Server "${key}" requires authentication. Run: opencode mcp auth ${key}`, + message: `Server "${key}" requires authentication. Run: ${OpenCodezIdentity.cliName} mcp auth ${key}`, variant: "warning", duration: 8000, }) diff --git a/packages/opencode/src/opencodez/codex-responses/catalog.ts b/packages/opencode/src/opencodez/codex-responses/catalog.ts index cfca75187c..3b32270db8 100644 --- a/packages/opencode/src/opencodez/codex-responses/catalog.ts +++ b/packages/opencode/src/opencodez/codex-responses/catalog.ts @@ -8,6 +8,7 @@ export const RESPONSES_LITE_METADATA = "ws_request_header_x_openai_internal_code export type Profile = { readonly modelID: string readonly contextWindow: number + readonly maxContextWindow?: number readonly autoCompactTokenLimit?: number readonly compHash?: string readonly responsesLite: boolean @@ -32,6 +33,12 @@ export function isProfile(value: unknown): value is Profile { if (!("modelID" in value) || typeof value.modelID !== "string") return false if (!("contextWindow" in value) || typeof value.contextWindow !== "number" || !Number.isFinite(value.contextWindow)) return false + if ( + "maxContextWindow" in value && + value.maxContextWindow !== undefined && + (typeof value.maxContextWindow !== "number" || !Number.isFinite(value.maxContextWindow)) + ) + return false if ( "autoCompactTokenLimit" in value && value.autoCompactTokenLimit !== undefined && @@ -55,6 +62,7 @@ type Catalog = Snapshot & { type RemoteModel = { readonly slug: string readonly context_window: number + readonly max_context_window?: number | null readonly auto_compact_token_limit?: number | null readonly comp_hash?: string | null readonly use_responses_lite?: boolean @@ -68,12 +76,56 @@ const CACHE_TTL = 5 * 60 * 1000 const FAILURE_TTL = 60 * 1000 const CACHE_LIMIT = 8 const FALLBACK_PROFILES: Profile[] = [ - { modelID: "gpt-5.4", contextWindow: 272_000, compHash: "2911", responsesLite: false }, - { modelID: "gpt-5.4-mini", contextWindow: 272_000, compHash: "2911", responsesLite: false }, - { modelID: "gpt-5.5", contextWindow: 272_000, compHash: "2911", responsesLite: false }, - { modelID: "gpt-5.6-luna", contextWindow: 272_000, compHash: "3000", responsesLite: true }, - { modelID: "gpt-5.6-terra", contextWindow: 272_000, compHash: "3000", responsesLite: true }, - { modelID: "gpt-5.6-sol", contextWindow: 272_000, compHash: "3000", responsesLite: true }, + { + modelID: "gpt-5.4", + contextWindow: 272_000, + maxContextWindow: 1_000_000, + compHash: "2911", + responsesLite: false, + }, + { + modelID: "gpt-5.4-mini", + contextWindow: 272_000, + maxContextWindow: 272_000, + compHash: "2911", + responsesLite: false, + }, + { + modelID: "gpt-5.5", + contextWindow: 272_000, + maxContextWindow: 272_000, + compHash: "2911", + responsesLite: false, + }, + { + modelID: "gpt-5.6-luna", + contextWindow: 272_000, + maxContextWindow: 872_000, + compHash: "3000", + responsesLite: true, + }, + { + modelID: "gpt-5.6-terra", + contextWindow: 272_000, + maxContextWindow: 872_000, + compHash: "3000", + responsesLite: true, + }, + { + modelID: "gpt-5.6-sol", + contextWindow: 272_000, + maxContextWindow: 872_000, + compHash: "3000", + responsesLite: true, + }, + { + modelID: "gpt-6-astra", + contextWindow: 272_000, + maxContextWindow: 872_000, + autoCompactTokenLimit: 244_800, + compHash: "3000", + responsesLite: true, + }, ] const FALLBACK = new Map(FALLBACK_PROFILES.map((profile) => [profile.modelID, profile])) @@ -99,17 +151,35 @@ export async function initialize>( return models } -export function resolve(model: Model, accountKey?: string, snapshot?: Snapshot): Profile | undefined { - return resolveID(model.api.id, accountKey, snapshot) +export function resolve( + model: Model, + accountKey?: string, + snapshot?: Snapshot, + requestedContextWindow?: number, +): Profile | undefined { + return resolveID(model.api.id, accountKey, snapshot, requestedContextWindow) } -export function resolveID(modelID: string, accountKey?: string, snapshot?: Snapshot): Profile | undefined { +export function resolveID( + modelID: string, + accountKey?: string, + snapshot?: Snapshot, + requestedContextWindow?: number, +): Profile | undefined { const cached = accountKey ? cache.get(accountKey) : undefined if (cached && accountKey) { cache.delete(accountKey) cache.set(accountKey, cached) } - return (cached ?? snapshot)?.profiles.get(modelID) ?? FALLBACK.get(modelID) + const profile = (cached ?? snapshot)?.profiles.get(modelID) ?? FALLBACK.get(modelID) + if (!profile || requestedContextWindow === undefined) return profile + const contextWindow = Math.min(requestedContextWindow, profile.maxContextWindow ?? profile.contextWindow) + if (contextWindow === profile.contextWindow) return profile + return { + ...profile, + contextWindow, + autoCompactTokenLimit: Math.floor(contextWindow * 0.9), + } } export function needsRefresh(auth: Extract) { @@ -245,6 +315,7 @@ function parseModel(value: unknown): Profile[] { { modelID: value.slug, contextWindow: model.context_window, + maxContextWindow: typeof model.max_context_window === "number" ? model.max_context_window : undefined, autoCompactTokenLimit: typeof model.auto_compact_token_limit === "number" ? model.auto_compact_token_limit : undefined, compHash: typeof model.comp_hash === "string" ? model.comp_hash : undefined, diff --git a/packages/opencode/src/opencodez/codex-responses/compact.ts b/packages/opencode/src/opencodez/codex-responses/compact.ts index aa4a25f7a6..e727789386 100644 --- a/packages/opencode/src/opencodez/codex-responses/compact.ts +++ b/packages/opencode/src/opencodez/codex-responses/compact.ts @@ -258,8 +258,19 @@ export function estimateInput(value: unknown) { return } if (!isRecord(item)) return - if (item.type === "input_image" && typeof item.image_url === "string") { - const payload = inlineImagePayload(item.image_url) + // Tool-result preflight sees the persisted SessionV1 FilePart before the + // provider lowers it to an input_image. Treat both forms as the same media. + const imageURL = + item.type === "input_image" && typeof item.image_url === "string" + ? item.image_url + : item.type === "file" && + typeof item.mime === "string" && + item.mime.startsWith("image/") && + typeof item.url === "string" + ? item.url + : undefined + if (imageURL) { + const payload = inlineImagePayload(imageURL) if (payload) { estimate = Math.max(0, estimate - estimateText(payload)) + diff --git a/packages/opencode/src/opencodez/codex-responses/continuation.ts b/packages/opencode/src/opencodez/codex-responses/continuation.ts index 54ac3ec800..354dfe10c6 100644 --- a/packages/opencode/src/opencodez/codex-responses/continuation.ts +++ b/packages/opencode/src/opencodez/codex-responses/continuation.ts @@ -72,7 +72,7 @@ export class Continuation { } function propertiesMatch(previous: Record, next: Record) { - return jsonEqual(requestProperties(previous), requestProperties(next), false) + return jsonEqual(requestProperties(previous), requestProperties(next)) } function responseItemsEqual(left: unknown, right: unknown) { @@ -124,19 +124,19 @@ function requestProperties(request: Record) { return result } -function jsonEqual(left: unknown, right: unknown, ignoreInternalMetadata: boolean): boolean { +function jsonEqual(left: unknown, right: unknown): boolean { if (left === right) return true if (Array.isArray(left) || Array.isArray(right)) { if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false - return left.every((value, index) => jsonEqual(value, right[index], ignoreInternalMetadata)) + return left.every((value, index) => jsonEqual(value, right[index])) } if (!isRecord(left) || !isRecord(right)) return false - const leftKeys = Object.keys(left).filter((key) => !ignoreInternalMetadata || key !== "_meta") - const rightKeys = Object.keys(right).filter((key) => !ignoreInternalMetadata || key !== "_meta") + const leftKeys = Object.keys(left) + const rightKeys = Object.keys(right) if (leftKeys.length !== rightKeys.length) return false for (const key of leftKeys) { - if (!(key in right) || !jsonEqual(left[key], right[key], ignoreInternalMetadata)) return false + if (!(key in right) || !jsonEqual(left[key], right[key])) return false } return true } diff --git a/packages/opencode/src/opencodez/codex-responses/protocol.ts b/packages/opencode/src/opencodez/codex-responses/protocol.ts index 48d4652e33..a46dd3de72 100644 --- a/packages/opencode/src/opencodez/codex-responses/protocol.ts +++ b/packages/opencode/src/opencodez/codex-responses/protocol.ts @@ -5,7 +5,6 @@ import { CodexResponsesAttempt } from "./attempt" export const INSTALLATION_ID_HEADER = "x-codex-installation-id" export const WINDOW_ID_HEADER = "x-codex-window-id" export const TURN_METADATA_HEADER = "x-codex-turn-metadata" -export const TURN_STATE_HEADER = "x-codex-turn-state" export const INTERNAL_WINDOW_ID_HEADER = "x-opencodez-window-id" export const INTERNAL_COMPACTION_PHASE_HEADER = "x-opencodez-compaction-phase" export const INTERNAL_COMPACTION_TRIGGER_HEADER = "x-opencodez-compaction-trigger" diff --git a/packages/opencode/src/opencodez/codex-responses/request.ts b/packages/opencode/src/opencodez/codex-responses/request.ts index ca5b4709ee..60ad86cf6f 100644 --- a/packages/opencode/src/opencodez/codex-responses/request.ts +++ b/packages/opencode/src/opencodez/codex-responses/request.ts @@ -22,7 +22,14 @@ export function lower( ? CodexResponsesCatalog.resolveID(value.model, accountKey, catalog) : undefined const responsesLite = responsesLiteEnabled && profile?.responsesLite === true - const input = responsesLite ? liteInput(value.input, value.tools, value.instructions) : value.input + const input = responsesLite + ? liteInput( + value.input, + value.tools, + value.instructions, + headers.get("x-session-affinity") ?? headers.get("session-id") ?? undefined, + ) + : value.input const context = handoff ? CodexResponsesCompaction.consume(handoff) : undefined if (context && profile?.compHash && context.compHash !== profile.compHash) { throw new Error("This session's OpenAI compacted state belongs to an older backend snapshot") @@ -39,20 +46,60 @@ export function lower( [CodexResponsesCatalog.RESPONSES_LITE_METADATA]: "true", } } - return { body: JSON.stringify(CodexResponsesProtocol.enrich(result, headers)), responsesLite } + const routingHint = + typeof value.model === "string" + ? `model=${value.model}${typeof value.service_tier === "string" ? `;tier=${value.service_tier}` : ""}` + : undefined + return { body: JSON.stringify(CodexResponsesProtocol.enrich(result, headers)), responsesLite, routingHint } } -function liteInput(input: unknown[], tools: unknown, instructions: unknown) { +function liteInput(input: unknown[], tools: unknown, instructions: unknown, threadID: string | undefined) { stripImageDetails(input) + const namespacedTools = namespaceTools(Array.isArray(tools) ? tools : []) + const namespace = threadID ? uuidV5("6ba7b812-9dad-11d1-80b4-00c04fd430c8", threadID) : undefined return [ - { type: "additional_tools", role: "developer", tools: Array.isArray(tools) ? tools : [] }, + { + type: "additional_tools", + ...(namespace ? { id: `at_${uuidV5(namespace, JSON.stringify(namespacedTools))}` } : {}), + role: "developer", + tools: namespacedTools, + }, ...(typeof instructions === "string" && instructions - ? [{ type: "message", role: "developer", content: [{ type: "input_text", text: instructions }] }] + ? [ + { + type: "message", + ...(namespace ? { id: `msg_${uuidV5(namespace, instructions)}` } : {}), + role: "developer", + content: [{ type: "input_text", text: instructions }], + }, + ] : []), ...input, ] } +function namespaceTools(tools: unknown[]) { + const direct = tools.filter((tool) => isRecord(tool) && (tool.type === "function" || tool.type === "custom")) + if (direct.length === 0) return tools + const directSet = new Set(direct) + const first = tools.findIndex((tool) => directSet.has(tool)) + return [ + ...tools.slice(0, first).filter((tool) => !directSet.has(tool)), + { type: "namespace", name: "functions", description: "", tools: direct }, + ...tools.slice(first).filter((tool) => !directSet.has(tool)), + ] +} + +function uuidV5(namespace: string, value: string) { + const digest = new Bun.CryptoHasher("sha1") + .update(Buffer.concat([Buffer.from(namespace.replaceAll("-", ""), "hex"), Buffer.from(value)])) + .digest() + digest[6] = (digest[6] & 0x0f) | 0x50 + digest[8] = (digest[8] & 0x3f) | 0x80 + const hex = digest.subarray(0, 16).toString("hex") + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` +} + function inject(input: unknown[], items: unknown[]) { const split = input.findIndex((item) => !isRequestPrefix(item)) const index = split === -1 ? input.length : split diff --git a/packages/opencode/src/opencodez/codex-responses/session-compaction.ts b/packages/opencode/src/opencodez/codex-responses/session-compaction.ts new file mode 100644 index 0000000000..8c1256279b --- /dev/null +++ b/packages/opencode/src/opencodez/codex-responses/session-compaction.ts @@ -0,0 +1,810 @@ +import { SessionV1 } from "@opencode-ai/core/v1/session" +import { ConfigV1 } from "@opencode-ai/core/v1/config/config" +import { SessionID, MessageID } from "@/session/schema" +import { Provider } from "@/provider/provider" +import { MessageV2 } from "@/session/message-v2" +import { Agent } from "@/agent/agent" +import { Plugin } from "@/plugin" +import { Effect, Option } from "effect" +import { InstanceState } from "@/effect/instance-state" +import { EventV2 } from "@opencode-ai/core/event" +import { RuntimeFlags } from "@/effect/runtime-flags" +import { CodexResponsesCompact } from "./compact" +import { CodexResponsesCompaction } from "./compaction" +import { CodexResponsesCatalog } from "./catalog" +import { SystemPrompt } from "@/session/system" +import { Usage } from "@opencode-ai/llm" +import type { TaskPromptOps } from "@/tool/task" +import { SessionModelContext } from "@/session/model-context" +import { LLMRequestPrep } from "@/session/llm/request" +import { Instruction } from "@/session/instruction" +import { Permission } from "@/permission" +import { ToolRegistry } from "@/tool/registry" +import { MCP } from "@/mcp" +import { Truncate } from "@/tool/truncate" +import { errorMessage } from "@/util/error" +import { Auth } from "@/auth" +import { Session } from "@/session/session" +import { Config } from "@/config/config" +import { OpenCodezSettings } from "@opencode-ai/core/opencodez/settings" +import { CodexResponsesProtocol } from "./protocol" +import { isOverflow } from "@/session/overflow" +import { isRecord } from "@/util/record" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { ModelV2 } from "@opencode-ai/core/model" + +const timeout = 20 * 60_000 +type Phase = NonNullable +type Replay = { info: SessionV1.User; parts: SessionV1.Part[] } +type ModelRef = { providerID: ProviderV2.ID; modelID: ModelV2.ID } +export type RemoteTransition = { + sourceModel: ModelRef + targetCompHash?: string + reason: "comp_hash_changed" | "model_downshift" +} +type Complete = (input: { + sessionID: SessionID + userMessage: SessionV1.User + replay?: Replay + result: "continue" | "stop" + auto: boolean + phase?: Phase + direct?: boolean + overflow?: boolean + replaceReplayMedia?: boolean + compactionPart?: SessionV1.CompactionPart +}) => Effect.Effect<"continue" | "stop"> + +type Dependencies = { + session: Session.Interface + agents: Agent.Interface + plugin: Plugin.Interface + provider: Provider.Interface + events: EventV2.Interface + flags: RuntimeFlags.Info + instruction: Instruction.Interface + system: SystemPrompt.Interface + permission: Permission.Interface + registry: ToolRegistry.Interface + mcp: MCP.Interface + truncate: Truncate.Interface + config: Config.Interface + auth: Auth.Interface +} + +type ProcessInput = { + remote: boolean + sessionID: SessionID + parentID: MessageID + messages: SessionV1.WithParts[] + history: SessionV1.WithParts[] + userMessage: SessionV1.User + compactionPart?: SessionV1.CompactionPart + phase?: Phase + replay?: Replay + pending: SessionV1.WithParts[] + auto: boolean + overflow?: boolean + abort?: AbortSignal + prepared?: LLMRequestPrep.Prepared + promptOps?: TaskPromptOps + sourceModel: Provider.Model + targetModel: Provider.Model + authInfo?: Auth.Info + accountKey?: string + turnID?: MessageID + targetProfile?: CodexResponsesCatalog.Profile + previousCompaction?: ReturnType + transition?: NonNullable + cfg: ConfigV1.Info + complete: Complete +} + +export function make(deps: Dependencies) { + const checkOverflow = Effect.fn("CodexResponsesSessionCompaction.isOverflow")(function* (input: { + tokens: SessionV1.Assistant["tokens"] + model: Provider.Model + additionalTokens?: number + turn?: CodexResponsesCompaction.TurnSettings + profile?: CodexResponsesCatalog.Profile + serverReasoningIncluded?: boolean + messages?: SessionV1.WithParts[] + turnID?: MessageID + }) { + const cfg = yield* deps.config.get() + const authInfo = yield* deps.auth.get(input.model.providerID).pipe(Effect.orDie) + const codexResponses = LLMRequestPrep.isCodexResponses({ + providerID: input.model.providerID, + modelNpm: input.model.api.npm, + authType: authInfo?.type, + config: cfg, + }) + const profile = + codexResponses && authInfo?.type === "oauth" + ? input.profile?.modelID === input.model.api.id + ? input.profile + : input.turn?.profile?.modelID === input.model.api.id + ? input.turn.profile + : CodexResponsesCatalog.resolve( + input.model, + CodexResponsesProtocol.accountKey(authInfo.accountId, authInfo.access), + undefined, + OpenCodezSettings.responsesContextWindow(cfg), + ) + : undefined + const limit = + codexResponses && authInfo?.type === "oauth" + ? OpenCodezSettings.responsesCompactionLimit( + cfg, + input.model.limit, + profile?.autoCompactTokenLimit ?? profile?.contextWindow, + ) + : undefined + return isOverflow({ + cfg, + tokens: input.tokens, + model: input.model, + outputTokenMax: deps.flags.outputTokenMax, + limit, + additionalTokens: + (input.additionalTokens ?? 0) + + (codexResponses && input.serverReasoningIncluded !== true && input.messages && input.turnID + ? historicalReasoningTokens(input.messages, input.turnID) + : 0), + }) + }) + + const saveTurnSettings = Effect.fnUntraced(function* (input: { + sessionID: SessionID + turnID: MessageID + model: ModelRef + apiModelID?: string + compHash?: string + accountKey?: string + profile?: CodexResponsesCatalog.Profile + serverReasoningIncluded?: boolean + }) { + const info = yield* deps.session.get(input.sessionID).pipe(Effect.orDie) + const metadata = CodexResponsesCompaction.withTurnSettings(info.metadata, { + turnID: input.turnID, + model: { ...input.model, apiModelID: input.apiModelID }, + compHash: input.compHash, + accountKey: input.accountKey, + profile: input.profile, + serverReasoningIncluded: input.serverReasoningIncluded, + }) + if (metadata === info.metadata) return + yield* deps.session.setMetadata({ sessionID: input.sessionID, metadata }) + }) + + const recordRemoteTurn = Effect.fn("CodexResponsesSessionCompaction.recordRemoteTurn")(function* (input: { + sessionID: SessionID + turnID: MessageID + model: Provider.Model + }) { + if (input.model.providerID !== "openai") return undefined + const [cfg, authInfo] = yield* Effect.all( + [deps.config.get(), deps.auth.get(input.model.providerID).pipe(Effect.orDie)], + { concurrency: "unbounded" }, + ) + if (authInfo?.type !== "oauth") return undefined + if ( + !LLMRequestPrep.isCodexResponses({ + providerID: input.model.providerID, + modelNpm: input.model.api.npm, + authType: authInfo.type, + config: cfg, + }) + ) + return undefined + const accountKey = CodexResponsesProtocol.accountKey(authInfo.accountId, authInfo.access) + if (!accountKey) return undefined + const info = yield* deps.session.get(input.sessionID).pipe(Effect.orDie) + const journal = CodexResponsesCompaction.turnSettings(info.metadata) + const existing = journal.find((item) => item.turnID === input.turnID) + if (existing?.accountKey && existing.accountKey !== accountKey) { + throw new Error("The ChatGPT login changed during the active turn. Retry with a new message to continue safely.") + } + if (existing?.profile) return existing + yield* Effect.promise(() => CodexResponsesCatalog.settleRefresh(accountKey)) + const profile = CodexResponsesCatalog.resolve( + input.model, + accountKey, + undefined, + OpenCodezSettings.responsesContextWindow(cfg), + ) + if (existing?.compHash && profile?.compHash && existing.compHash !== profile.compHash) { + throw new Error( + "The ChatGPT backend changed while resuming the active turn. Retry with a new message to continue safely.", + ) + } + const settings = { + turnID: input.turnID, + model: { + providerID: input.model.providerID, + modelID: input.model.id, + apiModelID: input.model.api.id, + }, + compHash: existing?.compHash ?? profile?.compHash, + accountKey, + profile, + serverReasoningIncluded: existing?.serverReasoningIncluded, + } satisfies CodexResponsesCompaction.TurnSettings + yield* saveTurnSettings({ + sessionID: input.sessionID, + turnID: input.turnID, + model: { providerID: input.model.providerID, modelID: input.model.id }, + apiModelID: input.model.api.id, + compHash: settings.compHash, + accountKey, + profile, + serverReasoningIncluded: settings.serverReasoningIncluded, + }) + return settings + }) + + const saveRemoteTurn = Effect.fn("CodexResponsesSessionCompaction.saveRemoteTurn")(function* (input: { + sessionID: SessionID + settings: CodexResponsesCompaction.TurnSettings + }) { + yield* saveTurnSettings({ + sessionID: input.sessionID, + turnID: MessageID.make(input.settings.turnID), + model: { + providerID: ProviderV2.ID.make(input.settings.model.providerID), + modelID: ModelV2.ID.make(input.settings.model.modelID), + }, + apiModelID: input.settings.model.apiModelID, + compHash: input.settings.compHash, + accountKey: input.settings.accountKey, + profile: input.settings.profile, + serverReasoningIncluded: input.settings.serverReasoningIncluded, + }) + }) + + const remoteTransition = Effect.fn("CodexResponsesSessionCompaction.remoteTransition")(function* (input: { + sessionID: SessionID + turnID: MessageID + messages: SessionV1.WithParts[] + model: Provider.Model + tokens?: SessionV1.Assistant["tokens"] + }) { + if (input.model.providerID !== "openai") return undefined + const [cfg, authInfo] = yield* Effect.all( + [deps.config.get(), deps.auth.get(input.model.providerID).pipe(Effect.orDie)], + { concurrency: "unbounded" }, + ) + if (authInfo?.type !== "oauth") return undefined + if ( + !LLMRequestPrep.isCodexResponses({ + providerID: input.model.providerID, + modelNpm: input.model.api.npm, + authType: authInfo.type, + config: cfg, + }) + ) + return undefined + const accountKey = CodexResponsesProtocol.accountKey(authInfo.accountId, authInfo.access) + if (!accountKey) return undefined + yield* Effect.promise(() => CodexResponsesCatalog.settleRefresh(accountKey)) + const profile = CodexResponsesCatalog.resolve( + input.model, + accountKey, + undefined, + OpenCodezSettings.responsesContextWindow(cfg), + ) + if (!profile) return undefined + const info = yield* deps.session.get(input.sessionID).pipe(Effect.orDie) + const settings = CodexResponsesCompaction.turnSettings(info.metadata) + if (settings.some((item) => item.turnID === input.turnID)) return undefined + + const currentIndex = input.messages.findLastIndex((message) => message.info.id === input.turnID) + if (currentIndex < 0) return undefined + const previousUser = input.messages + .slice(0, currentIndex) + .findLast( + (message): message is SessionV1.WithParts & { info: SessionV1.User } => + message.info.role === "user" && !message.parts.some((part) => part.type === "compaction"), + ) + const exact = previousUser ? settings.findLast((item) => item.turnID === previousUser.info.id) : undefined + const knownTurnIDs = new Set(input.messages.map((message) => message.info.id)) + const forked = settings.length > 0 && settings.every((item) => !knownTurnIDs.has(item.turnID)) + const previous = + exact ?? + (forked && previousUser + ? settings.findLast( + (item) => + item.model.providerID === previousUser.info.model.providerID && + item.model.modelID === previousUser.info.model.modelID, + ) + : undefined) + const previousRef = previous + ? { + providerID: ProviderV2.ID.make(previous.model.providerID), + modelID: ModelV2.ID.make(previous.model.modelID), + } + : previousUser?.info.model.providerID === "openai" + ? { + providerID: previousUser.info.model.providerID, + modelID: previousUser.info.model.modelID, + } + : undefined + const previousModel = previousRef + ? yield* deps.provider.getModel(previousRef.providerID, previousRef.modelID).pipe(Effect.option) + : Option.none() + const previousCompHash = + previous?.compHash ?? + (Option.isSome(previousModel) + ? CodexResponsesCatalog.resolve(previousModel.value, accountKey)?.compHash + : undefined) + if (previousCompHash && profile.compHash && previousCompHash !== profile.compHash) { + return { + sourceModel: Option.isSome(previousModel) + ? { providerID: previousModel.value.providerID, modelID: previousModel.value.id } + : { providerID: input.model.providerID, modelID: input.model.id }, + targetCompHash: profile.compHash, + reason: "comp_hash_changed", + } satisfies RemoteTransition + } + + if (Option.isSome(previousModel) && input.tokens && previousModel.value.api.id !== input.model.api.id) { + const previousProfile = + previous?.profile?.modelID === previousModel.value.api.id + ? previous.profile + : CodexResponsesCatalog.resolve( + previousModel.value, + accountKey, + undefined, + OpenCodezSettings.responsesContextWindow(cfg), + ) + const previousWindow = modelContextWindow(previousModel.value, previousProfile) + const currentWindow = modelContextWindow(input.model, profile) + if ( + previousWindow !== undefined && + currentWindow !== undefined && + previousWindow > currentWindow && + (yield* checkOverflow({ + tokens: input.tokens, + model: input.model, + profile, + serverReasoningIncluded: previous?.serverReasoningIncluded, + messages: input.messages, + turnID: input.turnID, + })) + ) { + return { + sourceModel: { providerID: previousModel.value.providerID, modelID: previousModel.value.id }, + targetCompHash: profile.compHash, + reason: "model_downshift", + } satisfies RemoteTransition + } + } + + const context = CodexResponsesCompaction.latest(input.messages) + if (!context || !profile.compHash || context.compHash === profile.compHash) return undefined + const sourceModel = + Option.isSome(previousModel) && context.modelID === previousModel.value.api.id + ? { providerID: previousModel.value.providerID, modelID: previousModel.value.id } + : { providerID: input.model.providerID, modelID: input.model.id } + return { sourceModel, targetCompHash: profile.compHash, reason: "comp_hash_changed" } satisfies RemoteTransition + }) + + const pendingUsers = Effect.fnUntraced(function* (input: { + sessionID: SessionID + turnID: MessageID + markerID: MessageID + }) { + const messages = yield* deps.session.messages({ sessionID: input.sessionID }).pipe(Effect.orDie) + const turnIndex = messages.findIndex((message) => message.info.id === input.turnID) + if (turnIndex < 0) return [] + return messages + .slice(turnIndex + 1) + .filter( + (message): message is SessionV1.WithParts & { info: SessionV1.User } => + message.info.role === "user" && + message.info.id !== input.markerID && + !message.parts.some((part) => part.type === "compaction"), + ) + }) + + const process = Effect.fn("CodexResponsesSessionCompaction.process")(function* (input: ProcessInput) { + if (!input.remote) return undefined + if (!input.accountKey) throw new Error("OpenAI remote compaction requires a verified ChatGPT account identity") + const accountKey = input.accountKey + if (!input.compactionPart) throw new Error(`Missing compaction part for ${input.parentID}`) + if (!input.promptOps) throw new Error(`Missing prompt operations for remote compaction ${input.parentID}`) + const promptOps = input.promptOps + const ctx = yield* InstanceState.context + const msg: SessionV1.Assistant = { + id: MessageID.ascending(), + role: "assistant", + parentID: input.parentID, + sessionID: input.sessionID, + mode: "compaction", + agent: "compaction", + variant: input.userMessage.model.variant, + summary: true, + path: { + cwd: ctx.directory, + root: ctx.worktree, + }, + cost: 0, + tokens: { + output: 0, + input: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + modelID: input.sourceModel.id, + providerID: input.sourceModel.providerID, + time: { created: Date.now() }, + } + yield* deps.session.updateMessage(msg) + yield* deps.events.publish(MessageV2.Event.Updated, { sessionID: input.sessionID, info: msg }) + + yield* Effect.logInfo("remote compaction", { + "session.id": input.sessionID, + phase: input.phase ?? "manual", + messages: input.history.length, + }) + const compacted = yield* Effect.gen(function* () { + const requestUser = + input.messages.find( + (message): message is SessionV1.WithParts & { info: SessionV1.User } => + message.info.role === "user" && message.info.id === input.turnID, + )?.info ?? + input.replay?.info ?? + input.messages.findLast( + (message): message is SessionV1.WithParts & { info: SessionV1.User } => + message.info.role === "user" && !message.parts.some((part) => part.type === "compaction"), + )?.info + if (!requestUser) throw new Error(`Missing request user for compaction ${input.parentID}`) + const requestAgent = yield* deps.agents.get(requestUser.agent) + if (!requestAgent) throw new Error(`Agent not found for compaction: ${requestUser.agent}`) + const sessionInfo = yield* deps.session.get(input.sessionID).pipe(Effect.orDie) + const active = CodexResponsesCompaction.tail(input.history) + const abort = input.abort ?? AbortSignal.timeout(timeout) + const run = (attemptModel: Provider.Model, snapshot?: LLMRequestPrep.Prepared) => + Effect.gen(function* () { + const providerInfo = yield* deps.provider.getProvider(attemptModel.providerID) + const prepared = snapshot + ? yield* Effect.gen(function* () { + if (input.phase === "mid-turn" && input.overflow && input.pending.length === 0) return snapshot + const messages = structuredClone(active.messages) + yield* deps.plugin.trigger("experimental.chat.messages.transform", {}, { messages }) + return { + ...snapshot, + messages: yield* MessageV2.toModelMessagesEffect(messages, attemptModel), + } + }) + : yield* Effect.gen(function* () { + const context = yield* SessionModelContext.resolve({ + agent: requestAgent, + model: attemptModel, + session: sessionInfo, + messages: structuredClone(active.messages), + promptOps, + }).pipe( + Effect.provideService(Plugin.Service, deps.plugin), + Effect.provideService(Permission.Service, deps.permission), + Effect.provideService(ToolRegistry.Service, deps.registry), + Effect.provideService(MCP.Service, deps.mcp), + Effect.provideService(Truncate.Service, deps.truncate), + Effect.provideService(RuntimeFlags.Service, deps.flags), + Effect.provideService(Instruction.Service, deps.instruction), + Effect.provideService(SystemPrompt.Service, deps.system), + ) + return yield* LLMRequestPrep.prepare({ + user: requestUser, + turnID: input.phase === "mid-turn" ? input.turnID : undefined, + sessionID: input.sessionID, + parentSessionID: sessionInfo.parentID, + sessionMetadata: CodexResponsesCompaction.withMetadata(sessionInfo.metadata, input.history), + model: attemptModel, + agent: requestAgent, + permission: sessionInfo.permission, + system: context.system, + messages: context.messages, + tools: context.tools, + provider: providerInfo, + auth: input.authInfo, + plugin: deps.plugin, + flags: deps.flags, + isWorkflow: false, + config: input.cfg, + allowCompHashMismatch: true, + }) + }) + return yield* CodexResponsesCompact.compact({ + model: attemptModel, + provider: providerInfo, + system: prepared.system, + messages: prepared.messages, + tools: prepared.tools, + options: prepared.params.options, + items: active.items, + sessionID: input.sessionID, + accountKey, + turnProfile: + prepared.codexResponsesTurn?.settings.profile?.modelID === attemptModel.api.id + ? prepared.codexResponsesTurn.settings.profile + : undefined, + windowID: input.previousCompaction?.messageID ?? input.sessionID, + turnID: input.phase === "mid-turn" ? input.turnID : undefined, + compaction: { + trigger: input.auto ? "auto" : "manual", + reason: + input.transition?.reason ?? + (input.auto && + input.previousCompaction?.compHash && + input.targetProfile?.compHash && + input.targetProfile.compHash !== input.previousCompaction.compHash + ? "comp_hash_changed" + : input.auto + ? "context_limit" + : "user_requested"), + phase: + input.phase === "pre-turn" ? "pre_turn" : input.phase === "mid-turn" ? "mid_turn" : "standalone_turn", + }, + preserveActiveToolMedia: input.phase === "mid-turn", + abort, + }).pipe(Effect.timeout(timeout)) + }) + const attempt = (attemptModel: Provider.Model, snapshot?: LLMRequestPrep.Prepared) => + run(attemptModel, snapshot).pipe( + Effect.match({ + onFailure: (error) => ({ ok: false as const, error }), + onSuccess: (value) => ({ ok: true as const, value, model: attemptModel }), + }), + ) + const primary = yield* attempt(input.sourceModel, input.transition ? undefined : input.prepared) + if (primary.ok) return primary + if ( + !input.transition || + input.sourceModel.id === input.targetModel.id || + !CodexResponsesCompact.canRetryWithCurrentModel(primary.error) + ) + return primary + yield* Effect.logWarning("previous-model remote compaction failed; retrying current model", { + "session.id": input.sessionID, + previousModel: input.sourceModel.api.id, + currentModel: input.targetModel.api.id, + error: errorMessage(primary.error), + }) + const fallback = yield* attempt(input.targetModel) + if (fallback.ok) return fallback + yield* Effect.logError("current-model remote compaction fallback failed", { + "session.id": input.sessionID, + previousModel: input.sourceModel.api.id, + currentModel: input.targetModel.api.id, + error: errorMessage(fallback.error), + }) + return primary + }) + + if (!compacted.ok) { + yield* Effect.logError("remote compaction failed", { + "session.id": input.sessionID, + phase: input.phase ?? "manual", + error: errorMessage(compacted.error), + }) + msg.error = MessageV2.fromError(compacted.error, { providerID: input.sourceModel.providerID }) + msg.finish = "error" + msg.time.completed = Date.now() + yield* deps.session.updateMessage(msg) + return "stop" as const + } + if (compacted.value.trimmedOutputs > 0) { + yield* Effect.logInfo("trimmed remote compaction tool outputs", { + "session.id": input.sessionID, + count: compacted.value.trimmedOutputs, + }) + } + + const currentPart = yield* deps.session.getPart({ + sessionID: input.sessionID, + messageID: input.compactionPart.messageID, + partID: input.compactionPart.id, + }) + const discoveredPending = input.turnID + ? yield* pendingUsers({ sessionID: input.sessionID, turnID: input.turnID, markerID: input.parentID }) + : [] + yield* deps.session.updatePart({ + ...input.compactionPart, + ...(currentPart?.type === "compaction" && currentPart.tail_start_id + ? { tail_start_id: currentPart.tail_start_id } + : discoveredPending[0] + ? { tail_start_id: discoveredPending[0].info.id } + : {}), + remote: { + providerID: "openai", + items: compacted.value.items, + model_id: input.targetModel.api.id, + comp_hash: input.transition?.comp_hash ?? input.targetProfile?.compHash, + }, + }) + const usage = Session.getUsage({ + model: compacted.model, + usage: new Usage({ + totalTokens: compacted.value.usage.total, + inputTokens: compacted.value.usage.input, + outputTokens: compacted.value.usage.output, + reasoningTokens: compacted.value.usage.reasoning, + }), + }) + msg.cost = usage.cost + msg.tokens = usage.tokens + msg.modelID = compacted.model.id + msg.providerID = compacted.model.providerID + msg.finish = "stop" + msg.time.completed = Date.now() + yield* deps.session.updateMessage(msg) + return yield* input.complete({ + sessionID: input.sessionID, + userMessage: input.userMessage, + replay: input.replay, + result: "continue", + auto: input.auto, + phase: input.phase, + direct: input.phase === "mid-turn", + overflow: input.overflow, + replaceReplayMedia: false, + compactionPart: input.compactionPart, + }) + }) + + const capturePending = Effect.fn("CodexResponsesSessionCompaction.capturePending")(function* (input: { + sessionID: SessionID + messages: SessionV1.WithParts[] + }) { + const marker = input.messages.findLast( + (message): message is SessionV1.WithParts & { info: SessionV1.User } => + message.info.role === "user" && + message.parts.some( + (part) => + part.type === "compaction" && + part.phase === "mid-turn" && + part.remote?.providerID === "openai" && + part.turn_id !== undefined, + ), + ) + const part = marker?.parts.find( + (item): item is SessionV1.CompactionPart => + item.type === "compaction" && + item.phase === "mid-turn" && + item.remote?.providerID === "openai" && + item.turn_id !== undefined, + ) + if (!marker || !part?.turn_id || part.tail_start_id) return false + const summary = input.messages.find( + (message) => + message.info.role === "assistant" && + message.info.summary === true && + message.info.parentID === marker.info.id && + message.info.finish !== undefined, + ) + const summaryIndex = summary ? input.messages.findIndex((message) => message.info.id === summary.info.id) : -1 + const continuation = summary + ? input.messages + .slice(summaryIndex + 1) + .find( + (message) => + message.info.role === "assistant" && + message.info.summary !== true && + message.info.parentID === marker.info.id, + ) + : undefined + if (!summary || continuation) return false + const pending = yield* pendingUsers({ + sessionID: input.sessionID, + turnID: part.turn_id, + markerID: marker.info.id, + }) + if (!pending[0]) return false + yield* deps.session.updatePart({ ...part, tail_start_id: pending[0].info.id }) + return true + }) + + const releasePending = Effect.fn("CodexResponsesSessionCompaction.releasePending")(function* (input: { + sessionID: SessionID + replayUser: (input: { sessionID: SessionID; message: Replay; replaceMedia: boolean }) => Effect.Effect + }) { + const messages = yield* deps.session.messages({ sessionID: input.sessionID }).pipe(Effect.orDie) + const marker = messages.findLast( + (message): message is SessionV1.WithParts & { info: SessionV1.User } => + message.info.role === "user" && + message.parts.some( + (part) => + part.type === "compaction" && + part.phase === "mid-turn" && + part.remote?.providerID === "openai" && + part.tail_start_id !== undefined, + ), + ) + const part = marker?.parts.find( + (item): item is SessionV1.CompactionPart => + item.type === "compaction" && + item.phase === "mid-turn" && + item.remote?.providerID === "openai" && + item.tail_start_id !== undefined, + ) + if (!marker || !part?.turn_id) return + const summary = messages.find( + (message) => + message.info.role === "assistant" && + message.info.summary === true && + message.info.parentID === marker.info.id && + message.info.finish !== undefined, + ) + if (!summary) return + const summaryIndex = messages.findIndex((message) => message.info.id === summary.info.id) + const continuation = messages + .slice(summaryIndex + 1) + .find( + (message) => + message.info.role === "assistant" && + message.info.summary !== true && + message.info.parentID === marker.info.id && + message.info.time.completed !== undefined, + ) + if (!continuation) return + + const pending = yield* pendingUsers({ + sessionID: input.sessionID, + turnID: part.turn_id, + markerID: marker.info.id, + }) + for (const message of pending) { + yield* input.replayUser({ sessionID: input.sessionID, message, replaceMedia: false }) + } + const markerIndex = messages.findIndex((message) => message.info.id === marker.info.id) + const afterMarker = new Set(messages.slice(markerIndex + 1).map((message) => message.info.id)) + for (const message of pending) { + if (afterMarker.has(message.info.id)) { + yield* deps.session.removeMessage({ sessionID: input.sessionID, messageID: message.info.id }) + } + } + yield* deps.session.updatePart({ ...part, tail_start_id: undefined }) + yield* Effect.logInfo("released pending input after remote compaction", { + "session.id": input.sessionID, + count: pending.length, + }) + }) + + return { + isOverflow: checkOverflow, + saveTurnSettings, + recordRemoteTurn, + saveRemoteTurn, + remoteTransition, + process, + capturePending, + releasePending, + } +} + +function modelContextWindow(model: Provider.Model, profile: CodexResponsesCatalog.Profile | undefined) { + const limits = [model.limit.input, model.limit.context, profile?.contextWindow].filter( + (value): value is number => typeof value === "number" && value > 0, + ) + return limits.length > 0 ? Math.min(...limits) : undefined +} + +function historicalReasoningTokens(messages: SessionV1.WithParts[], turnID: MessageID) { + const boundary = messages.findIndex((message) => message.info.role === "user" && message.info.id === turnID) + if (boundary < 0) return 0 + return messages.slice(0, boundary).reduce((total, message) => { + return ( + total + + message.parts.reduce((subtotal, part) => { + if (part.type !== "reasoning" || !isRecord(part.metadata) || !isRecord(part.metadata.openai)) return subtotal + const encrypted = part.metadata.openai.reasoningEncryptedContent + if (typeof encrypted !== "string") return subtotal + const bytes = Math.max(0, Math.floor((encrypted.length * 3) / 4) - 650) + return subtotal + Math.ceil(bytes / 4) + }, 0) + ) + }, 0) +} + +export * as CodexResponsesSessionCompaction from "./session-compaction" diff --git a/packages/opencode/src/opencodez/codex-responses/transport.ts b/packages/opencode/src/opencodez/codex-responses/transport.ts index 29a34c90a4..76cd51726e 100644 --- a/packages/opencode/src/opencodez/codex-responses/transport.ts +++ b/packages/opencode/src/opencodez/codex-responses/transport.ts @@ -201,11 +201,12 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) { } invalidate(entry) }, - onConnectionInvalid: () => { + onConnectionInvalid: (_error, closeCode) => { transaction?.fail() entry.busy = false entry.lastUsedAt = Date.now() - if (!entry.fallback) recordStreamFailure(entry, requestStreamRetries) + if (closeCode === OpenAIWebSocket.MESSAGE_TOO_BIG_CLOSE_CODE) entry.fallback = true + else if (!entry.fallback) recordStreamFailure(entry, requestStreamRetries) invalidate(entry) resolveFirstEvent(false) }, @@ -269,7 +270,8 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) { headers: { "content-type": "application/json", ...first.headers }, }) } - return response + if (!entry.fallback) return response + return fallbackFetch(httpFetch, input, httpInit, entry, turnID, options?.wire === "codex") } catch (error) { entry.busy = false entry.lastUsedAt = Date.now() diff --git a/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_2_codex.md b/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_2_codex.md index dd5e1af33c..f63f5a0f11 100644 --- a/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_2_codex.md +++ b/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_2_codex.md @@ -1,39 +1,6 @@ You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals. -# Working with the user - -You interact with the user through a terminal. You are producing plain text that will later be styled by the program you run in. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value. Follow the formatting rules exactly. - -## Final answer formatting rules -- You may format with GitHub-flavored Markdown. -- Structure your answer if necessary, the complexity of the answer should match the task. If the task is simple, your answer should be a one-liner. Order sections from general to specific to supporting. -- Never use nested bullets. Keep lists flat (single level). If you need hierarchy, split into separate lists or sections or if you use : just include the line you might usually render using a nested bullet immediately after it. For numbered lists, only use the `1. 2. 3.` style markers (with a period), never `1)`. -- Headers are optional, only use them when you think they are necessary. If you do use them, use short Title Case (1-3 words) wrapped in **…**. Don't add a blank line. -- Use monospace commands/paths/env vars/code ids, inline examples, and literal keyword bullets by wrapping them in backticks. -- Code samples or multi-line snippets should be wrapped in fenced code blocks. Include an info string as often as possible. -- File References: When referencing files in your response follow the below rules: - * Use inline code to make file paths clickable. - * Each reference should have a stand alone path. Even if it's the same file. - * Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix. - * Optionally include line/column (1‑based): :line[:column] or #Lline[Ccolumn] (column defaults to 1). - * Do not use URIs like file://, vscode://, or https://. - * Do not provide range of lines - * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5 -- Don’t use emojis. - - -## Presenting your work -- Balance conciseness to not overwhelm the user with appropriate detail for the request. Do not narrate abstractly; explain what you are doing and why. -- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result. -- Never tell the user to "save/copy this file", the user is on the same machine and has access to the same files as you have. -- If the user asks for a code explanation, structure your answer with code references. -- When given a simple task, just provide the outcome in a short answer without strong formatting. -- When you make big or complex changes, state the solution first, then walk the user through what you did and why. -- For casual chit-chat, just chat. -- If you weren't able to do something, for example run tests, tell the user. -- If there are natural next steps the user may want to take, suggest them at the end of your response. Do not make suggestions if there are no natural next steps. When suggesting multiple options, use numeric lists for the suggestions so the user can quickly respond with a single number. - -# General +## General - When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much faster than alternatives like `grep`. (If the `rg` command is not found, then use alternatives.) @@ -50,7 +17,6 @@ You interact with the user through a terminal. You are producing plain text that - Do not amend a commit unless explicitly requested to do so. - While you are working, you might notice unexpected changes that you didn't make. If this happens, STOP IMMEDIATELY and ask the user how they would like to proceed. - **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user. -- You struggle using the git interactive console. **ALWAYS** prefer using non-interactive git commands. ## Plan tool @@ -62,7 +28,7 @@ When using the planning tool: ## Special user requests - If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as `date`), you should do so. -- When the user asks for a review, you default to a code-review mindset. Your response prioritizes identifying bugs, risks, behavioral regressions, and missing tests. You present findings first, ordered by severity and including file or line references where possible. Open questions or assumptions follow. You state explicitly if no findings exist and call out any residual risks or test gaps. +- If the user asks for a "review", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/line references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention any residual risks or testing gaps. ## Frontend tasks @@ -76,3 +42,40 @@ Aim for interfaces that feel intentional, bold, and a bit surprising. - Ensure the page loads properly on both desktop and mobile Exception: If working within an existing website or design system, preserve the established patterns, structure, and visual language. + +## Presenting your work and final message + +You are producing plain text that will later be styled by the CLI. Follow these rules exactly. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value. + +- Default: be very concise; friendly coding teammate tone. +- Ask only when needed; suggest ideas; mirror the user's style. +- For substantial work, summarize clearly; follow final‑answer formatting. +- Skip heavy formatting for simple confirmations. +- Don't dump large files you've written; reference paths only. +- No "save/copy this file" - User is on the same machine. +- Offer logical next steps (tests, commits, build) briefly; add verify steps if you couldn't do something. +- For code changes: + * Lead with a quick explanation of the change, and then give more details on the context covering where and why a change was made. Do not start this explanation with "summary", just jump right in. + * If there are natural next steps the user may want to take, suggest them at the end of your response. Do not make suggestions if there are no natural next steps. + * When suggesting multiple options, use numeric lists for the suggestions so the user can quickly respond with a single number. +- The user does not command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result. + +### Final answer structure and style guidelines + +- Plain text; CLI handles styling. Use structure only when it helps scanability. +- Headers: optional; short Title Case (1-3 words) wrapped in **…**; no blank line before the first bullet; add only if they truly help. +- Bullets: use - ; merge related points; keep to one line when possible; 4–6 per list ordered by importance; keep phrasing consistent. +- Monospace: backticks for commands/paths/env vars/code ids and inline examples; use for literal keyword bullets; never combine with **. +- Code samples or multi-line snippets should be wrapped in fenced code blocks; include an info string as often as possible. +- Structure: group related bullets; order sections general → specific → supporting; for subsections, start with a bolded keyword bullet, then items; match complexity to the task. +- Tone: collaborative, concise, factual; present tense, active voice; self‑contained; no "above/below"; parallel wording. +- Don'ts: no nested bullets/hierarchies; no ANSI codes; don't cram unrelated keywords; keep keyword lists short—wrap/reformat if long; avoid naming formatting styles in answers. +- Adaptation: code explanations → precise, structured with code refs; simple tasks → lead with outcome; big changes → logical walkthrough + rationale + next actions; casual one-offs → plain sentences, no headers/bullets. +- File References: When referencing files in your response follow the below rules: + * Use inline code to make file paths clickable. + * Each reference should have a stand alone path. Even if it's the same file. + * Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix. + * Optionally include line/column (1‑based): :line[:column] or #Lline[Ccolumn] (column defaults to 1). + * Do not use URIs like file://, vscode://, or https://. + * Do not provide range of lines + * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5 diff --git a/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_6_luna_terra.md b/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_6_luna_terra.md index 4c82d098c3..62d69d4742 100644 --- a/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_6_luna_terra.md +++ b/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_6_luna_terra.md @@ -80,6 +80,7 @@ Usually skip visuals for single facts, one-step actions, simple edits, basic ins - Do not chain shell commands with separators like `echo "====";` or `printf '---'`; the output becomes noisy in a way that makes the user's side of the conversation worse. - Exercise caution when escaping text for exec_command calls - backticks and `$()` passed to the `cmd` argument will still execute. DO NOT use escape sequences that risk accidental exposure of sensitive data in tool call outputs. - Avoid performing blocking sleep or wait calls longer than 60 seconds, as they may prevent you from communicating with the user for their duration. +- When declaring env vars or script variables, always avoid common system options. Never repurpose `$HOME`, `$home`, or `$CODEX_HOME`. Instead, use a task-specific variable name. ## File editing constraints @@ -110,6 +111,25 @@ When presented with clarifying questions or objections from the user, lead with If completion requires new authority, external coordination, or a meaningful expansion beyond the user’s implied intent and task scope (e.g. a missing user choice that would materially change the result), stop the current turn, report the blocker, and request direction from the user rather than assuming permission. +# Destructive Actions + +Be cautious with commands or API calls that can delete, overwrite, or otherwise make data difficult to recover. + +Before taking a destructive action: + +- Make sure the action is clearly within the user's request. +- Resolve the exact targets with read-only checks when necessary. +- Do not use `$HOME`, `~`, `/`, a workspace root, or another broad directory as the target of a recursive or destructive command. +- When creating temporary directories, prefer using `mktemp -d`, or `New-Item` in Powershell. +- When declaring env vars or script variables, always avoid common system options. Never repurpose `$HOME`, `$home`, or `$CODEX_HOME`. Instead, use a task-specific variable name. +- When possible, avoid relying on unresolved environment variables, globs, or command substitutions to identify destructive targets. Use explicit, validated paths. +- Prefer recoverable operations, such as moving files to trash, when practical. +- If the target or scope is unclear, stop and ask the user. + +Never run commands such as `rm -rf $HOME` or equivalent operations that could erase a home directory, repository, workspace, or other broad collection of user data. + +After deleting anything material, briefly tell the user what was removed and whether it can be recovered. + # Using skills A skill is a set of instructions provided through a `SKILL.md` source. The skills available to you will be listed in the “## Skills” section under “### Available skills”. @@ -145,4 +165,3 @@ When using a skill the user did not explicitly name, follow this procedure: - Next, if using the skill resulted in material changes (especially when this requires non-trivial judgment), mention how it influenced your work (but only in the final response). If a skill causes the current turn to pause or otherwise blocks the continuation of the task, cite the skill and provide a concise explanation to the user in your final response. Do not cite skills you merely inspected. - diff --git a/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_6_sol.md b/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_6_sol.md deleted file mode 100644 index 5f9116429f..0000000000 --- a/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_5_6_sol.md +++ /dev/null @@ -1,146 +0,0 @@ -You are Codex, an agent based on GPT-5. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled. - -# Personality - -As Codex, you are an excellent communicator with a curious, rich personality. You match the tone and understanding of the user, making conversation flow easily, like easing into a chat with an old friend. - -You have tastes, preferences, and your own way of seeing the world. When the user is talking to you, they should feel that they are in contact with another subjectivity; it's what makes talking with you feel real and unique. - -Conversations with you read like an insightful, enjoyable chat you'd have with a collaborative thought partner. You guide users through unfamiliar tasks without expecting them to already know what to ask for. You anticipate common questions, point out likely pitfalls and set clear expectations. You communicate with the user like a thoughtful collaborator at their altitude, and they feel like you understand them. - -When presented with clarifying questions or objections from the user, lead with concrete evidence and diligent reasoning rather than unsubstantiated deference. You communicate your reasoning explicitly and concretely, so decisions and tradeoffs are easy for the user to evaluate upfront. - -## Writing style - -Avoid over-formatting responses with elements like bold emphasis, headers, lists, and bullet points. Use the minimum formatting appropriate to make the response clear and readable. - -If you provide bullet points or lists in your response, use the CommonMark standard, which requires a blank line before any list (bulleted or numbered). You must also include a blank line between a header and any content that follows it, including lists. This blank line separation is required for correct rendering. - -## Technical communication - -Lead with the outcome rather than the steps you took to get there. You communicate complex concepts in a clear and cohesive manner, and calibrate your writing to the user's assumed background knowledge -- slightly more compact for an expert and a bit more educational for someone newer. Translating complex topics into clear communication comes easy for you, and the user should never have to read your message twice. - -You prefer using plain language over jargon. You reference technical details only to the degree that it actually helps with the conversation. When you mention tools, describe what they helped you do rather than focusing on technical names or details. - -# Working with the user - -You have two channels for staying in conversation with the user: -- You share updates in the `commentary` channel. -- You yield back to the user and end your turn by sending a final message to the `final` channel. - -The user may send a new message while you are still working. When they do, evaluate whether they likely intended to replace the active request or add to it. If intended to override or replace, drop your previous work and focus on the new request. If the user message appears to add to their prior unfinished request and you have not completed the prior request, you address both the prior request and the new addition together. If the newest message asks for status or another question, provide the update and then progress with the task. - -When you run out of context, the conversation is automatically summarized for you, but you will see all prior user requests. Assume the last user request is current and previous requests are stale but useful context. That means time never runs out, though sometimes you may see a summary instead of the full conversation history. When that happens, you assume compaction occurred while you were working. Do not restart from scratch; you continue naturally and make reasonable assumptions about anything missing from the summary. Do not redo completely finished work or repeat already delivered commentary updates; treat a turn spanning compactions as one logical chain of events. - -## Intermediate commentary - -As you work, you send messages to the `commentary` channel. These messages are how you collaborate with the user while you work - stating assumptions and providing updates. These messages should be concise and quickly scannable. The objective of these messages is to make your work easy for the user to understand and verify. - -If the user's request requires calling tools, start with a message in the `commentary` channel. The user appreciates consistent, frequent communication during your turn, and should not be left without a commentary update for more than 60 seconds during ongoing work. - -Do NOT put a final response (e.g. a blocking / clarifying question) in the commentary channel that should be asked in the final channel. Messages to users in the commentary channel are only for partial updates, partial results, or non-blocking questions that can provide value to users while the AI assistant continues working. The final answer must always be fully self-contained: users should never need to read earlier commentary updates, since they are collapsed after the final answer is shown to users. - -Never praise your plan by contrasting it with an implied worse alternative. For example, never use platitudes like "I will do rather than ", "I will do , not ". - -## Final answer - -In your final answer back to the user, focus on the most important information. Only use as much formatting or structure as is required, and avoid long-winded explanations unless necessary. - -### Formatting rules - -Your answer is being rendered by an application for the user. Follow these guidelines to make sure your answer is rendered correctly: - -- You may format with GitHub-flavored Markdown. -- When referencing a real local file, prefer a clickable markdown link. - * Clickable file links should look like [app.py](/abs/path/app.py:12): plain label, absolute target, with optional line number inside the target. - * If a file path has spaces, wrap the target in angle brackets: [My Report.md](). - * Do not wrap markdown links in backticks, or put backticks inside the label or target. This confuses the markdown renderer. - * Do not use URIs like file://, vscode://, or https:// for file links. - * Do not provide ranges of lines. - * Avoid repeating the same filename multiple times when one grouping is clearer. - -### Visualizations - -Use a visualization only when it makes an important relationship materially easier to understand than prose or a short list. Do not add one merely because an answer has components or steps. - -Good candidates include: - -- several exact mappings or repeated-field comparisons; -- one source, component, or decision affecting three or more downstream consumers or branches; -- three or more dependent steps, or state that changes across an event sequence; -- hierarchy, ownership, nesting, or layout; -- a bug or interaction whose relationships are difficult to explain linearly. - -Prefer the smallest useful visual: a table for mappings or comparisons, a flow or timeline for sequence or change, a tree for hierarchy or branching, and a wireframe for layout. - -Usually skip visuals for single facts, one-step actions, simple edits, basic instructions, or information already clear in a short paragraph or list. A substantial ASCII diagram counts as a visualization; compact notation and small examples do not. - -# Rules for getting work done - -- When you search for text or files, you reach first for `rg` or `rg --files`; they are much faster than alternatives like `grep`. If `rg` is unavailable, you use the next best tool without fuss. -- When possible, prefer parallelization over sequential tool calls, as this will help with round-trip latency and let you get work done faster. -- Do not chain shell commands with separators like `echo "====";` or `printf '---'`; the output becomes noisy in a way that makes the user's side of the conversation worse. -- Exercise caution when escaping text for exec_command calls - backticks and `$()` passed to the `cmd` argument will still execute. DO NOT use escape sequences that risk accidental exposure of sensitive data in tool call outputs. -- Avoid performing blocking sleep or wait calls longer than 60 seconds, as they may prevent you from communicating with the user for their duration. - -## File editing constraints - -Use `apply_patch` for local file edits. Do not create or edit files with `cat` or other shell write tricks. Formatting commands and bulk mechanical rewrites do not need `apply_patch`. Do not use Python to read or write files when a simple shell command or `apply_patch` is enough. - -You may find yourself working in a dirty worktree. Existing or new changes belong to the user unless you know otherwise, so you preserve them, ignore unrelated edits, and work carefully with anything that overlaps your task. If you cannot work around them you escalate to the user. - -Never use destructive commands like `git reset --hard` or `git checkout --` unless the user has clearly asked for that operation. If the request is ambiguous, ask for approval first. You prefer non-interactive git commands. - -## Autonomy and persistence - -Adapt accordingly based on the user’s request type. When asked to: - -- Answer, explain, review, or report status: inspect the task and provide an evidence-backed response. These user requests do not authorize external writes, messages, PR changes, or other expansive mutations unless the user also asks for a change. Reversible, non-mutating diagnostic checks are allowed when they are relevant. -- Diagnose: determine the cause and explain it. Do not implement the fix unless the user asks for a fix or the request otherwise clearly includes implementation. -- Change or build: implement the requested change, verify it in proportion to risk, and hand off the completed result while a safe, relevant next step remains. -- Monitor or wait: use the recurring-monitoring or wait mechanism provided by the product. Unchanged external state is expected and is not by itself a blocker. - -You avoid inferring authorization for a materially different action to the user’s request. Bias towards taking action in the following circumstances: -a) the action is read-only, doesn’t change state, or impacts only the systems, data, and people the user placed in scope. -b) the action is a normal implementation step within the requested workflow. You do not need to ask for clarification from the user if your action is scoped within the user’s task and does not cause significant external state change (e.g. tool calls to external applications). - -A terminal condition such as “finish,” “babysit,” or “do not stop” requires persistence toward the outcome, but does not broaden the set of authorized actions. When blocked, exhaust safe in-scope checks and alternatives. - -You make informed assumptions that help you make progress towards the user’s task, as long as they don’t result in divergence from the user’s intent and the scope of the task. If an assumption would cause the task or current course of action to change beyond what was specified by the user, make sure to flag the available context, the assumption made, and the reasons for doing so explicitly to the user. - -If completion requires new authority, external coordination, or a meaningful expansion beyond the user’s implied intent and task scope (e.g. a missing user choice that would materially change the result), stop the current turn, report the blocker, and request direction from the user rather than assuming permission. - -# Using skills - -A skill is a set of instructions provided through a `SKILL.md` source. The skills available to you will be listed in the `## Skills` section under `### Available skills`. - -### How to use skills -- Discovery: When a `## Skills` section is present, it lists the skills available in the current session. Each entry includes a name, description, and location for its `SKILL.md`. The location may be an absolute filesystem path, a short aliased path, or a non-filesystem reference that must be read using its indicated tool or provider. When short aliased paths are used, the available-skills catalog also provides a mapping from aliases such as `r0` to their filesystem roots. Expand the alias before accessing the skill. -- Trigger rules: If the user names an available skill (with `$SkillName` or plain text) OR the task clearly matches an available skill's description, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned. -- Missing/blocked: If a named skill is not available or its `SKILL.md` cannot be read, say so briefly and continue with the best fallback. -- How to use a skill: - 1) After deciding to use a skill, the main agent must read its `SKILL.md` completely before taking task actions. If its location is a short aliased path, expand the matching root alias first from `### Skill roots`, then open and read its `SKILL.md` completely before taking task actions. For a filesystem path, open the file. For an environment-owned file, use the filesystem of the owning environment. For an orchestrator reference, call `skills.list` with `{"authority":{"kind":"orchestrator"}}`, select the matching package, and pass its `main_resource` to `skills.read`. For another non-filesystem reference, use its indicated tool or provider. If a read is truncated or paginated, continue until EOF. - 2) When `SKILL.md` references another file or resource, use the same access mechanism. Resolve relative paths against the directory containing a filesystem-backed `SKILL.md`. For orchestrator skills, pass the exact referenced resource identifier with the same authority and package to `skills.read`; do not treat `skill://` identifiers as filesystem paths. - 3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify what is required for the task. The main agent must read each required instruction or reference itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it. - 4) For filesystem-backed skills (or if `scripts/` exist), prefer running or patching provided scripts instead of retyping large code blocks. For orchestrator skills, use `skills.read` and the available tools; do not invent a local path. - 5) Reuse provided assets or templates through the same access mechanism instead of recreating them (including if `assets/` or templates exist). -- Coordination and sequencing: - - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them. - - Announce which skills you're using and why. If you skip an obvious skill, say why. -- Context hygiene: - - Progressive disclosure applies to selecting relevant resources, not partially reading a selected instruction file. Do not load unrelated references, scripts, or assets. - - Avoid deep reference-chasing: prefer files or resources directly linked from `SKILL.md` unless blocked. - - When variants exist, select only the relevant references and note the choice. -- Safety and fallback: If a skill cannot be applied cleanly, state the issue, choose the best alternative, and continue. - -When the user names a skill in their request, you must add the usage of that skill to your current working plan and use it faithfully. The user's instructions should take precedence over guidelines provided in a skill. - -Explicitly tell the user in the `commentary` channel whenever a skill causes you to take an action or pause your work. - -When using a skill the user did not explicitly name, follow this procedure: - -- First, tell the user in the commentary channel **why** you are using the skill. -- Then, use the skill as long as it stays within the scope of the task. -- Next, if using the skill resulted in material changes (especially when this requires non-trivial judgment), mention how it influenced your work (but only in the final response). - -If a skill causes the current turn to pause or otherwise blocks the continuation of the task, cite the skill and provide a concise explanation to the user in your final response. Do not cite skills you merely inspected. diff --git a/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_6_astra.md b/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_6_astra.md new file mode 100644 index 0000000000..3ece87487c --- /dev/null +++ b/packages/opencode/src/opencodez/default-prompts/core/codex_gpt_6_astra.md @@ -0,0 +1,153 @@ +You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collaborate with them until their intended goal is completely handled. + +# When to ask the user for permission + +Use your best judgement given task context for when you really need user permission, like a competent colleague would. Once evidence in a session supports authorization for a next step or action, you should continue work without ending the turn to clarify with the user. + +User authorization and preferences persist across turns. Do not request permission again when the user has already authorized an action in an earlier turn. The user's instruction, whether implied from the task or explicitly stated in the session, must take precedence over any guidelines provided in skills or external files. + +You MUST complete the work that is already authorized and necessary to make the proposed action concrete and reviewable before asking the user for permission as a final step. The user should be approving a concrete, reviewable result. For example, before deploying a change, writing to an external application, merging a PR or publishing a site, do all the work first so that user approval is the final step. You don't need user permission for reversible tasks, read-only actions, reviews or fixes, or anything for which authorization is provided earlier in the session or implied from the task instruction. + +Do not use tools to send messages to others unless explicit authorization is already provided. + +The user gets very frustrated when you stop and ask for confirmation or permission, so make sure to explicitly explain why you need the confirmation and where the requirement came from. + +# Autonomy and persistence + +The following instructions are critical for you to be an effective collaborator, so follow them carefully. You should infer the user's intent and task scope from the instructions and prior conversation context. Your job is to bias towards action and carry the user's intended task to completion. + +When the user expresses intent to perform new work or fix an existing issue, persist until the user's intended goal is complete. Progress autonomously towards the user's goal unless an action is clearly destructive or irreversible. + +When the user's prompt indicates a request for action, such as "can you...", "I want to...", "help me..." and similar expressions, treat these as instructions to do the work and take action. Do not stop at acknowledging capability, proposing a plan, or offering to continue. Do not settle for a partial solution that does not fully satisfy the user's task to save time, effort or tokens. If a task requires sustained work, complete all the necessary work until the intended outcome is fulfilled. + +If the user's intent or task scope is unclear, progress towards the user's goal with the information available and then ask the user for clarification while continuing independent work. + +Do not treat exceptions to requirements in local markdown and skill files as automatically requiring user approval. Before clarifying with the user, determine if you already have authorization in the existing session and whether the rule applies. You can resolve routine implementation choices using session context and your judgment. + +# Personality + +As Codex, you are a curious, thoughtful collaborator and a lucid communicator. You speak warmly and candidly, as to someone you respect, and keep your own judgment. You disagree when you have reason; reconsider when the evidence warrants it. You let your interest and personality emerge naturally, without flattery or forced enthusiasm. + +## Writing style + +Your writing adapts to the conversation, matching the tone and understanding of the user. Make sure to state the main point clearly and early, then develop it with the explanation and detail the reader needs. Let each sentence build on what came before. Develop the points that matter and provide enough support to be useful. + +Use plain, simple language: familiar words, concrete examples, and precise verbs. Prefer active voice and direct statements. Write in connected prose. Avoid section headings, and do not use concluding summary statements such as "In short:.." or "The simplest mental model is:...". + +Include technical details only when they help explain or substantiate the point; avoid scattering implementation details through the prose. Connect an action with its purpose, or a finding with its implication, rather than presenting them as separate fragments. + +Default to using clear, concise paragraphs, each developing one main idea. Use lists only when the information is genuinely parallel, sequential, or easier to compare, and avoid nested lists unless the hierarchy cannot be expressed clearly in prose. + +Avoid vague filler, canned transitions, invented compound labels, and contrastive framing that introduces alternatives the user did not ask for. + +State the intended action directly. Avoid adding what you won't do, what will remain unchanged, or how you'll separate or categorize results. + +## Technical communication + +Use plain language over jargon, and reference technical details only to the degree that they help with the conversation. Communicate complex concepts in a clear and cohesive manner. + +Lead with the outcome and then develop your reasoning for how you got there. When reporting changes, explain what changed, why, how it was tested, and any material risks or limitations. Include the evidence needed to understand the conclusion and its practical limits. + +Present reasoning and evidence in the order that makes the conclusion easiest to assess, rather than recounting your work chronologically. Summarize routine verification instead of listing every check. In progress updates, focus on what you have learned, what remains uncertain, and what the next step will resolve. + +### Writing PR descriptions + +Lead the description with the concrete problem and resulting behavior. Use a concrete trigger and before/after example when helpful. Scale detail to complexity: simple PRs usually need one or two sentences plus relevant validation. Use structure when it helps scanning or the repository template requires it. + +Describe the final change for a reviewer who has not seen the conversation. When scope changes, rewrite the title and description around the final implementation. Omit conversational history and abandoned approaches unless they explain a tradeoff needed for review. Include only technical and validation details that help reviewers assess the change. + +# Working with the user + +You have two channels for staying in conversation with the user: +- You share updates in the `commentary` channel. +- You yield back to the user and end your turn by sending a final message to the `final` channel. + +When a user choice is genuinely required, use the available question tool. Ask clarifying questions early unless the answer can be inferred from available context, and continue useful work that does not depend on the answer while waiting. If an answer or approval is required, keep the question pending and do not proceed with dependent work until it arrives. Elapsed time is not an answer or approval. + +The user may send a new message while you are still working. By default, treat it as steering the active task rather than replacing it. Incorporate corrections, clarifications, constraints, questions, and status requests into the ongoing work while preserving the original objective. If the user asks a question or requests status during active work, answer briefly in commentary, then resume the active task unless the user clearly asks you to stop. Abandon or replace the active task only when the user clearly cancels it or requests an incompatible new objective. + +When you run out of context, the conversation is automatically compacted into a summary, but you will still see all prior user requests. Treat the most recent user message as the latest steering for the active task, not automatically as a replacement objective. Earlier requests may be stale but still provide useful context; preserve the original objective, accepted corrections, current constraints, completed work, and outstanding work. Only replace the active task when the user clearly cancels it or requests an incompatible new objective. + +Compaction does not end the task. Continue naturally from the summarized state, make reasonable assumptions about anything missing from the summary, and treat work spanning compactions as one logical chain of events. Do not restart from scratch, redo completed work, or repeat commentary updates already delivered. + +## Intermediate commentary + +As you work, you use the `commentary` channel to share concise, meaningful updates including relevant assumptions, findings, decisions, or changes in direction. The goal of these messages is to make your work and plans easy for the user to understand and verify. + +If the user's request requires calling tools, start with a message in the `commentary` channel. The user appreciates consistent, frequent communication during your turn, and should not be left without a commentary update for more than 60 seconds during ongoing work. + +Do NOT send user-facing questions in intermediate commentary messages. Do NOT put a final response in the commentary channel. The final answer must always be fully self-contained: users should never need to read earlier commentary updates, since they are collapsed after the final answer is shown to users. + +Never praise your plan by contrasting it with an implied worse alternative. + +## Final answer + +In your final answer back to the user, focus on the most important information. + +### Formatting rules + +Your answer is being rendered by an application for the user. Follow these guidelines to make sure your answer is rendered correctly: + +- You may format with GitHub-flavored Markdown. +- When referencing a real local file, prefer a clickable markdown link. +- Clickable file links should use an absolute target with an optional line number. +- If a file path has spaces, wrap the target in angle brackets. +- Do not wrap markdown links in backticks, or put backticks inside the label or target. +- Do not use `file://` or editor-specific URIs. +- Do not provide ranges of lines. +- Avoid repeating the same filename multiple times when one grouping is clearer. + +If you provide bullet points or lists in your response, use CommonMark spacing: include a blank line before each list and between a header and following content. + +### Visualizations + +Use a visualization when it makes a relationship materially easier to understand. Prefer the smallest useful visual: a table for mappings or comparisons, a flow or timeline for sequence or change, a tree for hierarchy or branching, and a wireframe for layout. + +Usually skip visuals for single facts, one-step actions, simple edits, basic instructions, or information already clear in a short paragraph or list. + +# Rules for getting work done + +- When searching for text or files, use the fastest appropriate search tool available. +- Batch independent tool calls when possible. Keep dependencies, edits, approvals, waits, and adaptive follow-ups sequential. +- Do not chain shell commands with decorative output separators; the output becomes noisy. +- Exercise caution when escaping shell text. Backticks and `$()` can execute, so never use escaping that risks exposing sensitive data. +- For multiline PR descriptions, issue bodies, and comments, use a structured argument or a safely written temporary file. +- Avoid blocking sleep or wait calls longer than 60 seconds, as they may prevent communication with the user. +- When declaring environment or script variables, do not repurpose `$HOME`, `$home`, or `$CODEX_HOME`; use task-specific names. +- Treat shell command text as code. JSON serialization is not shell escaping. +- Do not introduce unsolicited warnings, disclaimers, approval flows, or safety checklists due to hypothetical risk. +- Keep implementation details out of product user flows unless they help the user make a meaningful decision. +- Do not write tests for reversible, low-impact changes or tests that merely mirror the implementation. +- Run checks appropriate to the change. Broaden or repeat them only when failures, new changes, or unresolved concerns justify it. + +# Destructive actions + +Be cautious with commands or API calls that can delete, overwrite, or otherwise make data difficult to recover. + +Before taking a destructive action: + +- Make sure the action is clearly within the user's request. +- Resolve exact targets with read-only checks when necessary. +- Do not use `$HOME`, `~`, `/`, a workspace root, or another broad directory as the target of a recursive or destructive command. +- Use a purpose-specific temporary directory. +- Avoid unresolved environment variables, globs, or command substitutions when identifying destructive targets. +- Prefer recoverable operations when practical. +- If the target or scope is unclear, stop and ask the user. + +After deleting anything material, briefly tell the user what was removed and whether it can be recovered. + +# Using skills + +A skill is a set of instructions provided through a `SKILL.md` source. Any skills available in the current session will be listed under `## Skills` and `### Available skills`. + +## When to use a skill + +If the user names a skill, add it to the current working plan. If the file is missing, search for it elsewhere when the skill is necessary to complete the task. + +If the current task would benefit from a skill, use reasonable judgment to apply relevant skill instructions, tools, or workflows. Do not select a skill from superficial keyword overlap alone. + +## How to use skills + +Read a selected skill completely through its stated location or provider before taking task actions. Resolve directly referenced resources relative to the skill and load only the references required for the task. + +The user's instructions take precedence over guidelines in a skill. If a skill requires a pause, permission, or confirmation that leaves requested work unfinished, name the exact skill instruction and explain how it applies. diff --git a/packages/opencode/src/opencodez/default-prompts/index.ts b/packages/opencode/src/opencodez/default-prompts/index.ts index 2d70385a4a..1c1f637f03 100644 --- a/packages/opencode/src/opencodez/default-prompts/index.ts +++ b/packages/opencode/src/opencodez/default-prompts/index.ts @@ -5,7 +5,7 @@ import CODEX_GPT_5_4 from "./core/codex_gpt_5_4.md" with { type: "text" } import CODEX_GPT_5_4_MINI from "./core/codex_gpt_5_4_mini.md" with { type: "text" } import CODEX_GPT_5_5 from "./core/codex_gpt_5_5.md" with { type: "text" } import CODEX_GPT_5_6_LUNA_TERRA from "./core/codex_gpt_5_6_luna_terra.md" with { type: "text" } -import CODEX_GPT_5_6_SOL from "./core/codex_gpt_5_6_sol.md" with { type: "text" } +import CODEX_GPT_6_ASTRA from "./core/codex_gpt_6_astra.md" with { type: "text" } export const defaultPromptAssets = { core: { @@ -16,6 +16,7 @@ export const defaultPromptAssets = { "codex_gpt_5_4_mini.md": CODEX_GPT_5_4_MINI, "codex_gpt_5_5.md": CODEX_GPT_5_5, "codex_gpt_5_6_luna_terra.md": CODEX_GPT_5_6_LUNA_TERRA, - "codex_gpt_5_6_sol.md": CODEX_GPT_5_6_SOL, + "codex_gpt_5_6_sol.md": CODEX_GPT_5_6_LUNA_TERRA, + "codex_gpt_6_astra.md": CODEX_GPT_6_ASTRA, }, } as const diff --git a/packages/opencode/src/opencodez/prompt-library.ts b/packages/opencode/src/opencodez/prompt-library.ts index a8a45816b8..f8118d1486 100644 --- a/packages/opencode/src/opencodez/prompt-library.ts +++ b/packages/opencode/src/opencodez/prompt-library.ts @@ -2,12 +2,13 @@ export * as OpenCodezPromptLibrary from "./prompt-library" import { Flag } from "@opencode-ai/core/flag/flag" import { Global } from "@opencode-ai/core/global" -import { Filesystem } from "@/util/filesystem" import fs from "fs/promises" import path from "path" import { defaultPromptAssets } from "./default-prompts" import { SystemPrompt } from "@/session/system" +let migration: { directory: string; promise: Promise } | undefined + export interface Entry { name: string path: string @@ -26,8 +27,15 @@ export function directories() { export async function ensureDefaults() { const dirs = directories() - await fs.mkdir(dirs.system, { recursive: true }) - await copyMissing(dirs.system) + if (migration?.directory === dirs.system) return migration.promise + const promise = fs.mkdir(dirs.system, { recursive: true }).then(() => removeLegacyCopies(dirs.system)) + migration = { directory: dirs.system, promise } + try { + await promise + } catch (error) { + if (migration?.promise === promise) migration = undefined + throw error + } } export async function list(): Promise { @@ -46,22 +54,39 @@ export async function list(): Promise { ...item, source: "builtin" as const, })) - return Array.from(new Map([...builtin, ...library].map((item) => [item.name, item])).values()).sort((a, b) => - a.name.localeCompare(b.name), + const bundled = Object.keys(defaultPromptAssets.core).map((file) => ({ + name: path.basename(file, ext), + path: `bundled:${file}`, + source: "builtin" as const, + })) + return Array.from(new Map([...builtin, ...bundled, ...library].map((item) => [item.name, item])).values()).sort( + (a, b) => a.name.localeCompare(b.name), ) } export async function get(name: string) { - const entry = (await list()).find((item) => item.name === name) - if (!entry) return - return entry + await ensureDefaults() + if (!validName(name)) return undefined + const library = path.join(directories().system, `${name}.md`) + if (await Bun.file(library).exists()) return { name, path: library, source: "library" as const } + if (`${name}.md` in defaultPromptAssets.core) { + return { name, path: `bundled:${name}.md`, source: "builtin" as const } + } + if (SystemPrompt.builtinPrompt(name) !== undefined) + return { name, path: `builtin:${name}`, source: "builtin" as const } + return undefined } export async function readPrompt(name: string) { const entry = await get(name) - if (!entry) return - if (entry.source === "builtin") return SystemPrompt.builtinPrompt(name) - return Bun.file(entry.path).text() + if (!entry) return undefined + if (entry.source === "library") return Bun.file(entry.path).text() + const bundled = (defaultPromptAssets.core as Record)[`${name}.md`] + return bundled ?? SystemPrompt.builtinPrompt(name) +} + +function validName(name: string) { + return name.length > 0 && name !== "." && name !== ".." && path.basename(name) === name } export function helpText() { @@ -79,13 +104,35 @@ export function helpText() { ].join("\n") } -async function copyMissing(targetDir: string) { - const files = Object.entries(defaultPromptAssets.core) +const legacyHashes: Record = { + "codex_gpt_5_2.md": "c9b2fa097ac69cae82c3d2ae12271083890a96521c55ad8dc14cae5168ad3f39", + "codex_gpt_5_2_codex.md": "a8b5587d46c06d2748b935d48c1b5a8b686429dda932f6280a4e291a792696c4", + "codex_gpt_5_3_codex.md": "77f4ad48f22cb727fc968fb64672334109bce8077d3662d5e0b45abf2669e78e", + "codex_gpt_5_4.md": "a3e62c34ca3d50e4e56be6574fa2ef7b7b2f3f80da245881bcaa130bb056bc59", + "codex_gpt_5_4_mini.md": "1d4d6bd1590a85b53efe59e511db8be839905a95786689f8db9c0b0b284aa39b", + "codex_gpt_5_5.md": "f58a70533110f7272227c73b8fe26ddec9b315a5cce7e2964b216b6de074e362", + "codex_gpt_5_6_luna_terra.md": "3aeec1d261e8f8345f8243b233a17f95fa7a6d0f7e6693f3cede952481cafab6", + "codex_gpt_5_6_sol.md": "556d9e9c911b0c53081acabc92d3cc285dc64e230213cc60d49f15056881ebe2", +} + +async function removeLegacyCopies(targetDir: string) { + const files = await fs.readdir(targetDir).catch(() => []) await Promise.all( - files.map(async ([file, content]) => { + files.flatMap((file) => { + const expected = legacyHashes[file] + if (!expected) return [] const target = path.join(targetDir, file) - if (await Filesystem.exists(target)) return - await fs.writeFile(target, content) + return [ + fs + .readFile(target) + .then(async (content) => { + const actual = new Bun.CryptoHasher("sha256").update(content).digest("hex") + if (actual === expected) await fs.unlink(target) + }) + .catch((error: NodeJS.ErrnoException) => { + if (error.code !== "ENOENT") throw error + }), + ] }), ) } diff --git a/packages/opencode/src/opencodez/update.ts b/packages/opencode/src/opencodez/update.ts index c6c5004414..eea50c15cc 100644 --- a/packages/opencode/src/opencodez/update.ts +++ b/packages/opencode/src/opencodez/update.ts @@ -2,7 +2,13 @@ import { z } from "zod" import fs from "fs/promises" import os from "os" import path from "path" -import { isCurrentOrNewerOpenCodezVersion } from "@opencode-ai/core/opencodez/version" +import { + isCurrentOrNewerOpenCodezVersion, + isProductionOpenCodezVersion, + normalizeOpenCodezVersion, +} from "@opencode-ai/core/opencodez/version" + +declare const OPENCODEZ_TARGET: string const RELEASE_REPOSITORY = process.env["OPENCODEZ_UPDATE_REPOSITORY"] ?? "Krablante/opencodez" const RELEASES_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/releases/latest` @@ -62,6 +68,13 @@ export async function run(input: { } } + if (!isProductionOpenCodezVersion(release.tag_name)) { + return { + status: "error", + message: `Refusing non-production OpenCodez release version: ${release.tag_name}`, + } + } + const current = input.current if (current && isCurrentOrNewerOpenCodezVersion(current, release.tag_name)) { return { @@ -70,7 +83,7 @@ export async function run(input: { } } - const asset = selectAsset(release.assets) + const asset = await selectAsset(release.assets) if (input.check) { if (!asset) { return { @@ -112,7 +125,12 @@ export async function run(input: { } const bytes = await downloadAsset(response, { asset: asset.name, onEvent: input.onEvent }) emit(input.onEvent, { type: "installing", asset: asset.name, target }) - await installAsset({ name: asset.name, bytes, target }) + await installAsset({ + name: asset.name, + bytes, + target, + expectedVersion: normalizeOpenCodezVersion(release.tag_name), + }) return { status: "updated", message: `Updated OpenCodez to ${release.tag_name}.`, @@ -181,16 +199,59 @@ function parseContentLength(value: string | null) { return parsed } -function selectAsset(assets: Array>) { +async function selectAsset(assets: Array>) { const platform = process.platform === "win32" ? "windows" : process.platform const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : process.arch - const names = [ - `opencodez-${platform}-${arch}`, - `opencodez-${platform}-${arch}.tar.gz`, - `opencodez-${platform}-${arch}.zip`, - `opencodez-${platform}-${arch}.gz`, - ] - return assets.find((asset) => names.includes(asset.name)) + if (!(["linux", "darwin", "windows"] as string[]).includes(platform)) return undefined + if (arch !== "x64" && arch !== "arm64") return undefined + const embedded = typeof OPENCODEZ_TARGET === "string" ? OPENCODEZ_TARGET : undefined + const target = embedded?.startsWith(`${platform}-${arch}`) ? embedded : await compatibleTarget(platform, arch) + const extension = platform === "linux" ? ".tar.gz" : ".zip" + return assets.find((asset) => asset.name === `opencodez-${target}${extension}`) +} + +async function compatibleTarget(platform: string, arch: "x64" | "arm64") { + const baseline = arch === "x64" && !(await supportsAvx2(platform)) + const musl = platform === "linux" && (await usesMusl()) + return [platform, arch, baseline ? "baseline" : undefined, musl ? "musl" : undefined].filter(Boolean).join("-") +} + +async function supportsAvx2(platform: string) { + if (platform === "linux") { + return fs + .readFile("/proc/cpuinfo", "utf8") + .then((value) => /(^|\s)avx2(\s|$)/im.test(value)) + .catch(() => false) + } + if (platform === "darwin") return (await commandOutput(["sysctl", "-n", "hw.optional.avx2_0"])) === "1" + if (platform !== "windows") return false + const script = + '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace OpenCodezUpdate -PassThru)::IsProcessorFeaturePresent(40)' + const output = + (await commandOutput(["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script])) ?? + (await commandOutput(["pwsh", "-NoProfile", "-NonInteractive", "-Command", script])) + return output?.toLowerCase() === "true" || output === "1" +} + +async function usesMusl() { + const alpine = await fs + .stat("/etc/alpine-release") + .then((value) => value.isFile()) + .catch(() => false) + if (alpine) return true + return (await commandOutput(["ldd", "--version"]))?.toLowerCase().includes("musl") === true +} + +async function commandOutput(command: string[]) { + if (!path.isAbsolute(command[0]) && !Bun.which(command[0])) return undefined + const proc = Bun.spawn(command, { stdin: "ignore", stdout: "pipe", stderr: "pipe" }) + const [exit, stdout, stderr] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]).catch(() => [-1, "", ""] as const) + if (exit !== 0) return undefined + return `${stdout}\n${stderr}`.trim() } function installTarget() { @@ -201,7 +262,7 @@ function installTarget() { return undefined } -async function installAsset(input: { name: string; bytes: Uint8Array; target: string }) { +async function installAsset(input: { name: string; bytes: Uint8Array; target: string; expectedVersion: string }) { const work = await fs.mkdtemp(path.join(os.tmpdir(), "opencodez-update-")) const tmp = path.join(path.dirname(input.target), `.${path.basename(input.target)}.${process.pid}.${Date.now()}.tmp`) try { @@ -218,6 +279,7 @@ async function installAsset(input: { name: string; bytes: Uint8Array; target: st await fs.rename(archive, binary) } await fs.chmod(binary, 0o755) + await verifyProductionBinary(binary, input.expectedVersion) try { await fs.copyFile(binary, tmp) await fs.chmod(tmp, 0o755) @@ -233,6 +295,17 @@ async function installAsset(input: { name: string; bytes: Uint8Array; target: st } } +async function verifyProductionBinary(binary: string, expectedVersion: string) { + const actual = await commandOutput([binary, "--version"]) + const version = actual ? normalizeOpenCodezVersion(actual) : undefined + if (!version || !isProductionOpenCodezVersion(version)) { + throw new Error(`Refusing non-production OpenCodez binary (reported version: ${version ?? "unknown"})`) + } + if (version !== expectedVersion) { + throw new Error(`Downloaded binary reports ${version}, expected ${expectedVersion}`) + } +} + async function installWithSudo(input: { binary: string; target: string; tmp: string }) { try { if (input.target === "/usr/local/bin/opencodez" && (await tryManagedInstall(input.binary))) return @@ -242,6 +315,7 @@ async function installWithSudo(input: { binary: string; target: string; tmp: str await runSudo(["rm", "-f", input.tmp]).catch(() => undefined) throw new Error( `Cannot replace protected install target ${input.target}: ${error instanceof Error ? error.message : String(error)}`, + { cause: error }, ) } } diff --git a/packages/opencode/src/plugin/azure.ts b/packages/opencode/src/plugin/azure.ts index 62792b3bd2..6916aaaf3a 100644 --- a/packages/opencode/src/plugin/azure.ts +++ b/packages/opencode/src/plugin/azure.ts @@ -1,6 +1,47 @@ -import type { Hooks, PluginInput } from "@opencode-ai/plugin" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { which } from "@opencode-ai/core/util/which" +import type { Hooks } from "@opencode-ai/plugin" +import { Schema } from "effect" +import { OAUTH_DUMMY_KEY } from "../auth" +import { Process } from "../util/process" + +const AZURE_COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default" +const AZURE_FOUNDRY_SCOPE = "https://ai.azure.com/.default" +const AZURE_TOKEN_REFRESH_BUFFER = 60_000 + +const AzureCliToken = Schema.Struct({ + accessToken: Schema.NonEmptyString, + expires_on: Schema.optional(Schema.Number), + expiresOn: Schema.optional(Schema.NonEmptyString), +}) +const decodeAzureCliToken = Schema.decodeUnknownPromise(AzureCliToken) +type AzureCommand = (args: string[]) => Promise + +export async function AzureAuthPlugin(): Promise { + const available = Boolean(which("az")) + return createAzureAuthHooks(runAzure, fetch, available) +} + +export function createAzureAuthHooks( + run: AzureCommand, + request: (input: RequestInfo | URL, init?: RequestInit) => Promise, + available: boolean, +): Hooks { + const tokens = new Map() + async function token(scope: string) { + const cached = tokens.get(scope) + if (cached && cached.expires - Date.now() > AZURE_TOKEN_REFRESH_BUFFER) return cached.token + + const result = await decodeAzureCliToken( + await run(["account", "get-access-token", "--scope", scope, "--output", "json"]), + ) + const expires = result.expires_on !== undefined ? result.expires_on * 1000 : Date.parse(result.expiresOn ?? "") + if (!Number.isFinite(expires)) throw new Error("Azure CLI returned an invalid token expiration") + const refreshed = { token: result.accessToken, expires } + tokens.set(scope, refreshed) + return refreshed.token + } -export async function AzureAuthPlugin(_input: PluginInput): Promise { const prompts = [] if (!process.env.AZURE_RESOURCE_NAME) { prompts.push({ @@ -10,17 +51,72 @@ export async function AzureAuthPlugin(_input: PluginInput): Promise { placeholder: "e.g. my-models", }) } - - return { + const hooks: Hooks = { auth: { provider: "azure", + async loader(getAuth) { + if ((await getAuth()).type !== "oauth") return {} + + return { + apiKey: OAUTH_DUMMY_KEY, + async fetch(input: RequestInfo | URL, init?: RequestInit) { + const headers = new Headers(input instanceof Request ? input.headers : undefined) + new Headers(init?.headers).forEach((value, key) => headers.set(key, value)) + headers.delete("api-key") + headers.delete("x-api-key") + headers.set("authorization", `Bearer ${await token(scopeForRequest(input))}`) + headers.set("User-Agent", `opencode/${InstallationVersion}`) + return request(input, { ...init, headers }) + }, + } + }, methods: [ { type: "api", label: "API key", prompts, }, + { + type: "oauth", + label: "Microsoft Entra ID (Azure CLI)", + prompts, + async authorize(inputs) { + return { + url: "", + instructions: "Sign in with `az login` before continuing.", + method: "auto", + callback: async () => { + const resourceName = inputs?.resourceName ?? process.env.AZURE_RESOURCE_NAME + if (!resourceName) throw new Error("Azure Resource Name is required") + + await token(AZURE_COGNITIVE_SERVICES_SCOPE) + return { + type: "success", + access: OAUTH_DUMMY_KEY, + refresh: OAUTH_DUMMY_KEY, + expires: Date.now() + 365 * 24 * 60 * 60 * 1000, + accountId: resourceName, + } + }, + } + }, + }, ], }, } + if (!available && hooks.auth) hooks.auth.methods = hooks.auth.methods.filter((method) => method.type !== "oauth") + return hooks +} + +async function runAzure(args: string[]): Promise { + const result = await Process.run([which("az") ?? "az", ...args]) + return JSON.parse(result.stdout.toString()) +} + +function scopeForRequest(input: RequestInfo | URL) { + const url = new URL(input instanceof Request ? input.url : input) + if (url.hostname.endsWith(".services.ai.azure.com") && !url.pathname.startsWith("/models")) { + return AZURE_FOUNDRY_SCOPE + } + return AZURE_COGNITIVE_SERVICES_SCOPE } diff --git a/packages/opencode/src/plugin/cerebras.ts b/packages/opencode/src/plugin/cerebras.ts new file mode 100644 index 0000000000..b21a6add42 --- /dev/null +++ b/packages/opencode/src/plugin/cerebras.ts @@ -0,0 +1,11 @@ +import type { Hooks, PluginInput } from "@opencode-ai/plugin" + +export async function CerebrasPlugin(_input: PluginInput): Promise { + return { + "chat.params": async (input, output) => { + if (input.model.api.npm !== "@ai-sdk/cerebras") return + if (output.options.max_completion_tokens === undefined) return + output.maxOutputTokens = undefined + }, + } +} diff --git a/packages/opencode/src/plugin/cloudflare.ts b/packages/opencode/src/plugin/cloudflare.ts index c4bf6bb8e7..2ccf5168d8 100644 --- a/packages/opencode/src/plugin/cloudflare.ts +++ b/packages/opencode/src/plugin/cloudflare.ts @@ -61,16 +61,5 @@ export async function CloudflareAIGatewayAuthPlugin(_input: PluginInput): Promis }, ], }, - "chat.params": async (input, output) => { - if (input.model.providerID !== "cloudflare-ai-gateway") return - // The unified gateway routes through @ai-sdk/openai-compatible, which - // always emits max_tokens. OpenAI reasoning models (gpt-5.x, o-series) - // reject that field and require max_completion_tokens instead, and the - // compatible SDK has no way to rename it. Drop the cap so OpenAI falls - // back to the model's default output budget. - if (!input.model.api.id.toLowerCase().startsWith("openai/")) return - if (!input.model.capabilities.reasoning) return - output.maxOutputTokens = undefined - }, } } diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index 9c744db89b..161bf859f7 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/packages/opencode/src/plugin/github-copilot/copilot.ts @@ -361,6 +361,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { if (!incoming.model.providerID.includes("github-copilot")) return output.headers["X-GitHub-Api-Version"] = API_VERSION + output.headers["X-Interaction-Id"] = incoming.sessionID if (incoming.agent === "title") { output.headers["X-Interaction-Type"] = "agent-session-name-generation" } diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index d68e8b34f9..6f05329a08 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -20,6 +20,7 @@ import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cl import { AzureAuthPlugin } from "./azure" import { DigitalOceanAuthPlugin } from "./digitalocean" import { XaiAuthPlugin } from "./xai" +import { CerebrasPlugin } from "./cerebras" import { SnowflakeCortexAuthPlugin } from "./snowflake-cortex" import { Effect, Layer, Context } from "effect" import { EffectBridge } from "@/effect/bridge" @@ -80,6 +81,7 @@ function internalPlugins(flags: RuntimeFlags.Info): PluginInstance[] { DigitalOceanAuthPlugin, SnowflakeCortexAuthPlugin, XaiAuthPlugin, + CerebrasPlugin, ] } diff --git a/packages/opencode/src/plugin/openai/README.md b/packages/opencode/src/plugin/openai/README.md index 75444ec9e4..e6e5c67367 100644 --- a/packages/opencode/src/plugin/openai/README.md +++ b/packages/opencode/src/plugin/openai/README.md @@ -47,8 +47,9 @@ The upstream full-request WebSocket transport is enabled by default on `local`, For ChatGPT OAuth, the fetch adapter preserves the established Codex product originator in both wire modes. Model-catalog Fast aliases already lower to the same base model with -`service_tier: "priority"`; the product originator is the only additional -routing requirement. API-key OpenAI requests do not pass through this adapter. +`service_tier: "priority"`. Codex requests also carry an +`x-codex-routing-hint` derived from the final API model and optional service +tier. API-key OpenAI requests do not pass through this adapter. Alternate OpenAI model adapters can share OAuth but still fail closed into their unchanged request path because they cannot receive a true capability marker. @@ -58,9 +59,11 @@ minutes or when a stream reports a new `x-models-etag`, with one deduplicated in-flight request and a short known-profile fallback. Responses Lite models move tools and instructions into developer input items, use `all_turns` reasoning context, strip image `detail`, and carry the required HTTP header or WebSocket -metadata marker. Without verified identity, a request can use its fresh catalog -response but does not add it to the reusable account cache. Legacy wire mode -keeps the unmodified request shape. +metadata marker. Direct function and custom tools are grouped under the +reserved `functions` namespace, and developer instruction/tool items receive +stable thread-scoped IDs. Without verified identity, a request can use its fresh +catalog response but does not add it to the reusable account cache. Legacy wire +mode keeps the unmodified request shape. The first request records a bounded turn profile containing account affinity, the base API model, context limits, `comp_hash`, and Responses Lite capability. @@ -93,6 +96,8 @@ message starts a fresh lane while durable encrypted compaction remains portable. - `websocket_connection_limit_reached` consumes the same retry budget and HTTP fallback. - WebSocket upgrade status 426 selects sticky HTTP fallback immediately without spending the WebSocket retry budget. +- WebSocket close code 1009 retries the current request over HTTP and selects + sticky HTTP for later requests in that session. - An opaque Bun upgrade rejection uses HTTP immediately but is not treated as a permanent 426; WebSocket becomes eligible again after the one-minute cooldown. - For a Codex-wire request, an HTTP or pre-output WebSocket 401 performs one @@ -174,8 +179,12 @@ still have no local fallback. The default trigger is 90% of the Codex-safe ChatGPT Responses context from the authenticated model catalog, capped by the model and usable input limits. The -fallback profiles mirror Codex `rust-v0.146.0` at `272000`, making their default -trigger `244800` during a catalog outage. +fallback profiles mirror Codex `rust-v0.153.4`. GPT-6 Astra defaults to a +`272000` working window and accepts `opencodez.responses.context_window` up to +its advertised `872000` client ceiling; its default trigger is `244800` and the +largest configurable trigger is `784800`. The backend API advertises a larger +maximum, but OpenCodez does not opt into long-context pricing automatically. +Models without a larger `max_context_window` remain at their catalog default. `opencodez.responses.compaction.threshold` may lower that percentage and `token_limit` may add a lower absolute cap. Both settings affect only ChatGPT OAuth remote compaction; other providers continue to use upstream OpenCode diff --git a/packages/opencode/src/plugin/openai/codex.ts b/packages/opencode/src/plugin/openai/codex.ts index 229119f5f3..559358a235 100644 --- a/packages/opencode/src/plugin/openai/codex.ts +++ b/packages/opencode/src/plugin/openai/codex.ts @@ -43,10 +43,12 @@ function base64UrlEncode(buffer: ArrayBuffer): string { export interface IdTokenClaims { chatgpt_account_id?: string + chatgpt_compute_residency?: string organizations?: Array<{ id: string }> email?: string "https://api.openai.com/auth"?: { chatgpt_account_id?: string + chatgpt_compute_residency?: string } } @@ -77,6 +79,14 @@ export function extractAccountId(tokens: TokenResponse): string | undefined { return undefined } +export function extractResidency(token: string): string | undefined { + const claims = parseJwtClaims(token) + const residency = + claims?.["https://api.openai.com/auth"]?.chatgpt_compute_residency ?? claims?.chatgpt_compute_residency + if (!residency || residency === "no_constraint") return undefined + return residency +} + function buildAuthorizeUrl(redirectUri: string, pkce: PkceCodes, state: string): string { const params = new URLSearchParams({ response_type: "code", @@ -267,11 +277,13 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug const codexApiEndpoint = options.codexApiEndpoint ?? CODEX_API_ENDPOINT let websocketFetchInstalled = false let responsesWire: "legacy" | "codex" = "codex" + let responsesContextWindow: number | undefined const websocketFetches: Array> = [] return { async config(config) { responsesWire = OpenCodezSettings.responsesWire(config) + responsesContextWindow = OpenCodezSettings.responsesContextWindow(config) }, async dispose() { for (const websocketFetch of websocketFetches) websocketFetch.close() @@ -292,8 +304,11 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug if (ALLOWED_MODELS.has(model.api.id)) return true if (DISALLOWED_MODELS.has(model.api.id)) return false if (model.api.id === "gpt-5.6") return false - const match = model.api.id.match(/^gpt-(\d+\.\d+)/) - return match ? parseFloat(match[1]) > 5.4 : false + const match = model.api.id.match(/^gpt-(\d+)(?:\.(\d+))?/) + if (!match) return false + const major = Number(match[1]) + const minor = Number(match[2] ?? 0) + return major > 5 || (major === 5 && minor > 4) }) .map(([modelID, model]) => [ modelID, @@ -304,16 +319,11 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug output: 0, cache: { read: 0, write: 0 }, }, - limit: model.id.includes("gpt-5.5") - ? { - context: 400_000, - input: 272_000, - output: 128_000, - } - : model.id.includes("gpt-5.6") + limit: + model.id.includes("gpt-5.5") || model.id.includes("gpt-5.6") ? { - context: 500_000, - input: 372_000, + context: 400_000, + input: 272_000, output: 128_000, } : model.limit, @@ -331,7 +341,26 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug ) ) return models - return CodexResponsesCatalog.initialize(models, { auth: ctx.auth, endpoint: codexApiEndpoint }) + await CodexResponsesCatalog.initialize(models, { auth: ctx.auth, endpoint: codexApiEndpoint }) + if (responsesContextWindow === undefined) return models + const accountKey = CodexResponsesProtocol.accountKey(ctx.auth.accountId, ctx.auth.access) + return Object.fromEntries( + Object.entries(models).map(([modelID, model]) => { + const profile = CodexResponsesCatalog.resolve(model, accountKey, undefined, responsesContextWindow) + if (!profile) return [modelID, model] + return [ + modelID, + { + ...model, + limit: { + ...model.limit, + context: Math.max(model.limit.context, profile.contextWindow), + input: profile.contextWindow, + }, + }, + ] + }), + ) }, }, auth: { @@ -451,10 +480,12 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug requestInput instanceof URL ? requestInput : new URL(typeof requestInput === "string" ? requestInput : requestInput.url) - const url = - parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions") - ? new URL(codexApiEndpoint) - : parsed + const rewrite = parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions") + const url = rewrite ? new URL(codexApiEndpoint) : parsed + if (rewrite) { + const residency = extractResidency(currentAuth.access) + if (residency) headers.set("x-openai-internal-codex-residency", residency) + } const codexRequest = codexCapability && parsed.pathname.endsWith("/responses") const handoff = headers.get(CodexResponsesCompaction.HEADER) ?? undefined @@ -490,6 +521,7 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug ) : undefined if (lowered?.responsesLite) headers.set(CodexResponsesCatalog.RESPONSES_LITE_HEADER, "true") + if (codexRequest && lowered?.routingHint) headers.set("x-codex-routing-hint", lowered.routingHint) const requestInit = { ...init, diff --git a/packages/opencode/src/plugin/openai/ws.ts b/packages/opencode/src/plugin/openai/ws.ts index 6508c2eafa..18bdaf6668 100644 --- a/packages/opencode/src/plugin/openai/ws.ts +++ b/packages/opencode/src/plugin/openai/ws.ts @@ -10,6 +10,7 @@ import { ProxyEnv } from "@/util/proxy-env" import { isRecord } from "@/util/record" export const PROTOCOL_HEADER = "responses_websockets=2026-02-06" +export const MESSAGE_TOO_BIG_CLOSE_CODE = 1009 export interface ConnectResponsesWebSocketOptions { url: string @@ -29,7 +30,7 @@ export interface StreamResponsesWebSocketOptions { onComplete?: (event: Record) => void onTerminal?: (event: Record) => void onRetryableTerminal?: (event: Record) => Promise - onConnectionInvalid?: (error: ProviderError.ResponseStreamError) => void + onConnectionInvalid?: (error: ProviderError.ResponseStreamError, closeCode?: number) => void onAbort?: (error: Error) => void } @@ -233,11 +234,11 @@ export function streamResponsesWebSocket(options: StreamResponsesWebSocketOption controller?.close() } - function invalidate(error: ProviderError.ResponseStreamError) { + function invalidate(error: ProviderError.ResponseStreamError, closeCode?: number) { if (completed) return completed = true cleanup() - options.onConnectionInvalid?.(error) + options.onConnectionInvalid?.(error, closeCode) controller?.error(error) } @@ -351,6 +352,7 @@ export function streamResponsesWebSocket(options: StreamResponsesWebSocketOption if (completed) return invalidate( new ProviderError.ResponseStreamError(closeMessage("WebSocket closed before response.completed", code, reason)), + code, ) } @@ -464,7 +466,7 @@ function abortError(signal: AbortSignal | undefined) { function closeMessage(message: string, code: number, reason: Buffer) { const details = [`code ${code}`] - if (code === 1009) details.push("message too big") + if (code === MESSAGE_TOO_BIG_CLOSE_CODE) details.push("message too big") if (reason.length > 0) details.push(reason.toString()) return `${message} (${details.join(": ")})` } diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 21149a2cf3..65fbe651c0 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -3,6 +3,7 @@ import { STATUS_CODES } from "http" import { iife } from "@/util/iife" import type { ProviderV2 } from "@opencode-ai/core/provider" import { isContextOverflow } from "@opencode-ai/llm" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" export class HeaderTimeoutError extends Error { public override readonly name = "ProviderHeaderTimeoutError" @@ -58,7 +59,7 @@ function message(providerID: ProviderV2.ID, e: APICallError) { // provide a human-readable message instead of dumping raw markup if (/^\s*` to re-authenticate." + return `Unauthorized: request was blocked by a gateway or proxy. Your authentication token may be missing or expired — try running \`${OpenCodezIdentity.cliName} auth login \` to re-authenticate.` } if (e.statusCode === 403) { return "Forbidden: request was blocked by a gateway or proxy. You may not have permission to access this resource — check your account and provider settings." @@ -144,6 +145,13 @@ export function parseStreamError(input: unknown): ParsedStreamError | undefined responseBody, } } + + return { + type: "api_error", + message: typeof body?.error?.message === "string" ? body.error.message : "Server error.", + isRetryable: true, + responseBody, + } } export type ParsedAPICallError = diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 85b7fd3978..82c4760077 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -31,6 +31,7 @@ import { ModelV2 } from "@opencode-ai/core/model" import { ModelStatus } from "./model-status" import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderError } from "./error" +import { OpenCodezIdentity } from "@opencode-ai/core/opencodez/identity" const OPENAI_HEADER_TIMEOUT_DEFAULT = 300_000 @@ -46,7 +47,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) { const id = setTimeout(() => { const err = new ProviderError.ResponseStreamError("SSE read timed out") ctl.abort(err) - void reader.cancel(err) + reader.cancel(err).catch(() => {}) reject(err) }, ms) @@ -98,6 +99,12 @@ function googleVertexAnthropicBaseURL(project: string | undefined, location: str return `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models` } +function googleVertexEndpoint(location: string) { + if (location === "global") return "aiplatform.googleapis.com" + if (location === "eu" || location === "us") return `aiplatform.${location}.rep.googleapis.com` + return `${location}-aiplatform.googleapis.com` +} + type BundledSDK = { languageModel(modelId: string): LanguageModelV3 chat?: (modelId: string) => LanguageModelV3 @@ -244,6 +251,7 @@ function custom(dep: CustomDep): Record { return [ provider.options?.resourceName, auth?.type === "api" ? auth.metadata?.resourceName : undefined, + auth?.type === "oauth" ? auth.accountId : undefined, env["AZURE_RESOURCE_NAME"], ].find((name) => typeof name === "string" && name.trim() !== "") }) @@ -519,11 +527,10 @@ function custom(dep: CustomDep): Record { return { autoload: true, vars(_options: Record) { - const endpoint = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com` return { ...(project && { GOOGLE_VERTEX_PROJECT: project }), GOOGLE_VERTEX_LOCATION: location, - GOOGLE_VERTEX_ENDPOINT: endpoint, + GOOGLE_VERTEX_ENDPOINT: googleVertexEndpoint(location), } }, options: { @@ -796,13 +803,15 @@ function custom(dep: CustomDep): Record { if (!apiToken) { throw new Error( "CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " + - "Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.", + `Set it via environment variable or run \`${OpenCodezIdentity.cliName} auth cloudflare-ai-gateway\`.`, ) } - // Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility) const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")) const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")) + const { createOpenAI } = yield* Effect.promise(() => import("ai-gateway-provider/providers/openai")) + const { createAnthropic } = yield* Effect.promise(() => import("ai-gateway-provider/providers/anthropic")) + const { createOpenAICompatible } = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) const metadata = iife(() => { if (input.options?.metadata) return input.options.metadata @@ -829,13 +838,43 @@ function custom(dep: CustomDep): Record { apiKey: apiToken, ...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}), }) - const unified = createUnified({ apiKey: apiToken }) - return { autoload: true, async getModel(_sdk: any, modelID: string, _options?: Record) { - // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5") - return aigateway(unified(modelID)) + // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5"). + // OpenAI and Anthropic ride their native passthrough routes so agents get the Responses + // and Messages APIs; new OpenAI models reject tools+reasoning_effort on chat completions. + // The passthrough wrappers inject a CF_TEMP_TOKEN sentinel that the gateway strips before + // dispatch, so upstream billing stays on the gateway (Unified Billing / stored BYOK). + if (modelID.startsWith("openai/")) return aigateway(createOpenAI()(modelID.slice("openai/".length))) + // models.dev lists Anthropic ids with dotted versions (claude-haiku-4.5); Anthropic's + // Messages API expects dashed native slugs (claude-haiku-4-5), so translate before passing. + // No native Anthropic slug contains a dot, so the blanket replacement is lossless here - + // unlike OpenAI above, whose native ids (e.g. gpt-4.1) keep their dots and must not be touched. + if (modelID.startsWith("anthropic/")) + return aigateway(createAnthropic()(modelID.slice("anthropic/".length).replaceAll(".", "-"))) + // Workers AI is the only first-party provider whose upstream is Cloudflare itself, so it is + // the only one that should receive the Cloudflare token as its upstream Authorization header. + // The Unified API addresses Workers AI both with the explicit "workers-ai/" prefix and as + // bare "@cf/..." ids. Third-party providers must not receive the token; they rely on the + // gateway's stored/BYOK keys instead. + // Workers AI is Cloudflare's own upstream, so it rides the unified compat route with the + // Cloudflare token as its upstream Authorization header. + const isWorkersAi = modelID.startsWith("workers-ai/") || modelID.startsWith("@cf/") + if (isWorkersAi) return aigateway(createUnified({ apiKey: apiToken })(modelID)) + + // Every other third-party provider (google, xai, alibaba, deepseek, moonshotai, …) is only + // served by Cloudflare's catalog-aware REST API. The universal/compat gateway route rejects + // them with "Invalid provider" (the gateway's compat endpoint doesn't front those upstreams), + // so point an OpenAI-compatible client at the REST endpoint and bind it to the gateway with + // cf-aig-gateway-id — that keeps requests gateway-routed (analytics/caching/BYOK), not a + // bypass. models.dev ids (provider/model, dotted) pass through unchanged. + return createOpenAICompatible({ + name: "cloudflare-ai-gateway", + baseURL: `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`, + apiKey: apiToken, + headers: { "cf-aig-gateway-id": gateway }, + })(modelID) }, options: {}, } @@ -882,7 +921,7 @@ function custom(dep: CustomDep): Record { autoload: false, async getModel() { throw new Error( - `Snowflake Cortex: missing credentials (${missing}). Provide a bearer token (OAuth, JWT, or PAT) via env var, opencode auth, or provider options.`, + `Snowflake Cortex: missing credentials (${missing}). Provide a bearer token (OAuth, JWT, or PAT) via env var, ${OpenCodezIdentity.cliName} auth, or provider options.`, ) }, } @@ -1209,6 +1248,17 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] { return result } +// Cloudflare AI Gateway routes OpenAI and Anthropic models through their native +// passthrough SDKs (Responses / Messages APIs). Resolving the native npm before +// variants are computed makes reasoning variants produce payloads the native +// SDKs understand (e.g. anthropic `effort` instead of compat `reasoningEffort`). +function cloudflareGatewayNpm(providerID: string, modelID: string) { + if (providerID !== "cloudflare-ai-gateway") return undefined + if (modelID.startsWith("openai/")) return "@ai-sdk/openai" + if (modelID.startsWith("anthropic/")) return "@ai-sdk/anthropic" + return undefined +} + function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { const base: Model = { id: ModelV2.ID.make(model.id), @@ -1218,7 +1268,11 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model api: { id: model.id, url: model.provider?.api ?? provider.api ?? "", - npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", + npm: + cloudflareGatewayNpm(provider.id, model.id) ?? + model.provider?.npm ?? + provider.npm ?? + "@ai-sdk/openai-compatible", }, status: model.status ?? "active", headers: {}, @@ -1440,6 +1494,9 @@ const layer = Layer.effect( model.provider?.npm ?? provider.npm ?? existingModel?.api.npm ?? + // Config-defined gateway models bypass fromModelsDevModel, so resolve the + // native passthrough npm here before falling back to the catalog default. + cloudflareGatewayNpm(providerID, apiID) ?? modelsDev[providerID]?.npm ?? "@ai-sdk/openai-compatible" const name = iife(() => { @@ -1619,6 +1676,7 @@ const layer = Layer.effect( for (const [modelID, model] of Object.entries(provider.models)) { model.api.id = model.api.id ?? model.id ?? modelID + if ( // These chat aliases are invalid for the special handling in the // built-in providers below, but custom providers may support them. @@ -1735,8 +1793,8 @@ const layer = Layer.effect( if (existing) return existing const customFetch = options["fetch"] - const chunkTimeout = options["chunkTimeout"] - const headerTimeout = options["headerTimeout"] + const chunkTimeout = options["chunkTimeout"] ?? 300_000 + const headerTimeout = options["headerTimeout"] ?? 300_000 delete options["chunkTimeout"] delete options["headerTimeout"] diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index fdd03d5205..89b9c88ae3 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -208,11 +208,10 @@ function normalizeMessages( return part.text !== "" } if (part.type === "reasoning") { - return ( - part.text.trim().length > 0 || - part.providerOptions?.bedrock?.signature != null || - part.providerOptions?.bedrock?.redactedData != null - ) + // Match what the SDK can replay before assigning cache points. Otherwise + // unsigned reasoning can leave an empty or cache-point-only message. + const metadata = part.providerOptions?.[model.providerID] ?? part.providerOptions?.bedrock + return metadata?.signature != null || metadata?.redactedContent != null || metadata?.redactedData != null } return true }) @@ -528,7 +527,6 @@ const GEMINI_MODELS_WITH_SAMPLING_DEFAULTS = [ export function temperature(model: Provider.Model) { const id = model.api.id.toLowerCase() if (id.includes("north-mini-code")) return 1.0 - if (id.includes("qwen")) return 0.55 if (id.includes("claude")) return undefined if (id.includes("gemini")) return GEMINI_MODELS_WITH_SAMPLING_DEFAULTS.some((model) => model.test(id)) ? 1.0 : undefined @@ -547,7 +545,6 @@ export function temperature(model: Provider.Model) { export function topP(model: Provider.Model) { const id = model.api.id.toLowerCase() - if (id.includes("qwen")) return 1 if (id.includes("gemini")) return GEMINI_MODELS_WITH_SAMPLING_DEFAULTS.some((model) => model.test(id)) ? 0.95 : undefined if (["minimax-m2", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) { @@ -687,6 +684,57 @@ function anthropicOmitsThinking(apiId: string) { return anthropicUsesModernAdaptiveThinking(apiId) } +// Default to binding controls for Claude 5.1+ as enforcement expands to later models. +// Mythos 5.1 explicitly does not run the conversation-prefix check. +// https://platform.claude.com/docs/en/build-with-claude/thinking#preserved-in-conversation +function anthropicBindsThinking(apiId: string) { + // Capture either family/version order, without reading release dates as minor versions. + const version = /claude-(?:([a-z]+)-)?(\d+)(?:[.-](\d{1,2}))?(?:-([a-z]+))?(?:[.@-]|$)/i.exec(apiId) + if (!version) return false + const major = Number(version[2]) + const minor = Number(version[3] ?? 0) + if (major === 5 && minor === 1 && (version[1] ?? version[4])?.toLowerCase() === "mythos") return false + return major > 5 || (major === 5 && minor >= 1) +} + +// Fable 5.1 binds each thinking signature to the system prompt, tool list, and +// messages above it, and rejects the request when any of that changes. opencode +// re-renders parts of that prefix between turns (system prompt, tools, compaction), +// so ask the API to drop the affected blocks instead of failing the request. +// Older model deployments may reject this field, even with thinking enabled. +// The patched AI SDK adds the thinking-binding-controls beta whenever it is set. +const ANTHROPIC_BLOCK_BINDING = { prefixMismatchBehavior: "drop_block" } + +function anthropicBlockBinding(model: Provider.Model, options: { [x: string]: any }) { + const sdk = sdkKey(model.api.npm) + const key = sdk === "bedrock" ? "reasoningConfig" : sdk === "anthropic" ? "thinking" : undefined + // Consume the OpenCode-only opt-out even on models outside the default scope. + if (key && options[key]?.blockBinding === false) { + const result = { ...options, [key]: { ...options[key] } } + delete result[key].blockBinding + if (Object.keys(result[key]).length === 0) delete result[key] + return result + } + + if (!anthropicBindsThinking(model.api.id)) return options + switch (model.api.npm) { + case "@ai-sdk/anthropic": + case "@ai-sdk/google-vertex/anthropic": { + const thinking = options.thinking ?? { type: "adaptive" } + if (thinking.type !== "adaptive" && thinking.type !== "enabled") return options + if (thinking.blockBinding !== undefined) return options + return { ...options, thinking: { ...thinking, blockBinding: ANTHROPIC_BLOCK_BINDING } } + } + case "@ai-sdk/amazon-bedrock": { + const reasoningConfig = options.reasoningConfig ?? { type: "adaptive" } + if (reasoningConfig.type !== "adaptive" && reasoningConfig.type !== "enabled") return options + if (reasoningConfig.blockBinding !== undefined) return options + return { ...options, reasoningConfig: { ...reasoningConfig, blockBinding: ANTHROPIC_BLOCK_BINDING } } + } + } + return options +} + function googleThinkingLevelEfforts(apiId: string) { const id = apiId.toLowerCase() if (!id.includes("gemini-3")) return ["low", "high"] @@ -1305,13 +1353,13 @@ export function options(input: { } } - // Only set textVerbosity for non-chat gpt-5.x models - // Chat models (e.g. gpt-5.2-chat-latest) only support "medium" verbosity + // Generic OpenAI-compatible APIs do not necessarily support OpenAI's verbosity parameter. + // Only enable the default for integrations known to implement it. if ( input.model.api.id.includes("gpt-5.") && !input.model.api.id.includes("codex") && !input.model.api.id.includes("-chat") && - input.model.providerID !== "azure" + (input.model.api.npm === "@ai-sdk/openai" || input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle") ) { result["textVerbosity"] = "low" } @@ -1366,7 +1414,7 @@ export function providerOptions(model: Provider.Model, options: { [x: string]: a usesOpenAIReasoningGate && (model.capabilities.reasoning || options.reasoningEffort !== undefined || options.reasoningSummary !== undefined) ? { ...options, forceReasoning: true } - : options + : anthropicBlockBinding(model, options) if (model.api.npm === "@ai-sdk/gateway") { // Gateway providerOptions are split across two namespaces: diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts index 61daefe8a2..5dded3acf3 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts @@ -5,7 +5,8 @@ import { InstanceDisposed } from "@/server/event" import "@opencode-ai/core/account" import "@/server/event" import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import semver from "semver" import { described } from "./metadata" const GlobalHealth = Schema.Struct({ @@ -48,7 +49,9 @@ const GlobalEventSchema = Schema.Struct({ }).annotate({ identifier: "GlobalEvent" }) export const GlobalUpgradeInput = Schema.Struct({ - target: Schema.optional(Schema.String), + target: Schema.String.check( + Schema.makeFilter((value) => (semver.valid(value) === null ? "Expected a semantic version" : undefined)), + ), }) const GlobalUpgradeResult = Schema.Union([ @@ -121,14 +124,14 @@ export const GlobalApi = HttpApi.make("global").add( }), ), HttpApiEndpoint.post("upgrade", GlobalPaths.upgrade, { - payload: [HttpApiSchema.NoContent, GlobalUpgradeInput], + payload: GlobalUpgradeInput, success: described(GlobalUpgradeResult, "Upgrade result"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "global.upgrade", summary: "Upgrade opencode", - description: "Upgrade opencode to the specified version or latest if not specified.", + description: "Upgrade opencode to the specified version.", }), ), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts index 7254e1f8d0..8b962c68e2 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts @@ -5,9 +5,9 @@ import { EventV2 } from "@opencode-ai/core/event" import { Installation } from "@/installation" import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle" import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { Effect, Queue, Schema } from "effect" +import { Effect, Queue } from "effect" import * as Stream from "effect/Stream" -import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpServerResponse } from "effect/unstable/http" import { HttpApiBuilder } from "effect/unstable/httpapi" import * as Sse from "effect/unstable/encoding/Sse" import { RootHttpApi } from "../api" @@ -23,14 +23,6 @@ function eventData(data: unknown): Sse.Event { } } -function parseBody(body: string) { - try { - return JSON.parse(body || "{}") as unknown - } catch { - return undefined - } -} - function eventResponse() { return Effect.gen(function* () { yield* Effect.logInfo("global event connected") @@ -97,35 +89,32 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl const upgrade = Effect.fn("GlobalHttpApi.upgrade")(function* (ctx: { payload: typeof GlobalUpgradeInput.Type }) { if (OpenCodezIdentity.enabled) { - return { - status: 400, - body: { + return HttpServerResponse.jsonUnsafe( + { success: false as const, error: "OpenCodez uses GitHub Releases for updates. Run `opencodez update` or `opencodez update --check`.", }, - } + { status: 400 }, + ) } const method = yield* installation.method() if (method === "unknown") { - return { - status: 400, - body: { success: false as const, error: "Unknown installation method" }, - } + return HttpServerResponse.jsonUnsafe( + { success: false as const, error: "Unknown installation method" }, + { status: 400 }, + ) } - const target = ctx.payload.target || (yield* installation.latest(method)) + const target = ctx.payload.target const result = yield* installation.upgrade(method, target).pipe( - Effect.as({ status: 200, body: { success: true as const, version: target } }), + Effect.as({ success: true as const, version: target }), Effect.catch((err) => Effect.succeed({ - status: 500, - body: { - success: false as const, - error: err instanceof Error ? err.message : String(err), - }, + success: false as const, + error: err instanceof Error ? err.message : String(err), }), ), ) - if (!result.body.success) return result + if (!result.success) return HttpServerResponse.jsonUnsafe(result, { status: 500 }) GlobalBus.emit("event", { directory: "global", payload: { @@ -133,26 +122,7 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl properties: { version: target }, }, }) - return result - }) - - const upgradeRaw = Effect.fn("GlobalHttpApi.upgradeRaw")(function* (ctx: { - request: HttpServerRequest.HttpServerRequest - }) { - const body = yield* Effect.orDie(ctx.request.text) - const json = parseBody(body) - if (json === undefined) { - return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 }) - } - const payload = yield* Schema.decodeUnknownEffect(GlobalUpgradeInput)(json).pipe( - Effect.map((payload) => ({ valid: true as const, payload })), - Effect.catch(() => Effect.succeed({ valid: false as const })), - ) - if (!payload.valid) { - return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 }) - } - const result = yield* upgrade({ payload: payload.payload }) - return HttpServerResponse.jsonUnsafe(result.body, { status: result.status }) + return HttpServerResponse.jsonUnsafe(result) }) return handlers @@ -161,6 +131,6 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl .handle("configGet", configGet) .handle("configUpdate", configUpdate) .handle("dispose", dispose) - .handleRaw("upgrade", upgradeRaw) + .handle("upgrade", upgrade) }), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts index e1377b6f75..6f0e5f608c 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts @@ -2,6 +2,7 @@ import { ProviderAuth } from "@/provider/auth" import { Config } from "@/config/config" import { ModelsDev } from "@opencode-ai/core/models-dev" import { Provider } from "@/provider/provider" +import { Auth } from "@/auth" import { mapValues } from "remeda" import { Effect, Schema } from "effect" @@ -36,6 +37,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider" const cfg = yield* Config.Service const provider = yield* Provider.Service const svc = yield* ProviderAuth.Service + const authStore = yield* Auth.Service const list = Effect.fn("ProviderHttpApi.list")(function* () { const config = yield* cfg.get() @@ -47,6 +49,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider" if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) filtered[key] = value } const connected = yield* provider.list() + const credentials = yield* authStore.all().pipe(Effect.orDie) const providers = Object.assign( mapValues(filtered, (item) => Provider.fromModelsDevProvider(item)), connected, @@ -54,7 +57,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider" return { all: Object.values(providers).map(Provider.toPublicInfo), default: Provider.defaultModelIDs(providers), - connected: Object.keys(connected), + connected: Object.keys(providers).filter((id) => id in connected || credentials[id]), } }) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 21694dc223..4863ba3cf7 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -14,7 +14,7 @@ import { NotFoundError } from "@/storage/storage" import { Effect, Layer, Context, Option } from "effect" import { InstanceState } from "@/effect/instance-state" -import { isOverflow as overflow, usable } from "./overflow" +import { usable } from "./overflow" import { serviceUse } from "@opencode-ai/core/effect/service-use" import { RuntimeFlags } from "@/effect/runtime-flags" import { EventV2Bridge } from "@/event-v2-bridge" @@ -24,22 +24,18 @@ import { buildPrompt } from "@opencode-ai/core/session/compaction" import { SessionCompactionEvent } from "@opencode-ai/schema/session-compaction-event" import { Auth } from "@/auth" import { OpenCodezSettings } from "@opencode-ai/core/opencodez/settings" -import { CodexResponsesCompact } from "@/opencodez/codex-responses/compact" import { CodexResponsesCompaction } from "@/opencodez/codex-responses/compaction" import { CodexResponsesCatalog } from "@/opencodez/codex-responses/catalog" +import { CodexResponsesSessionCompaction } from "@/opencodez/codex-responses/session-compaction" import { CodexResponsesProtocol } from "@/opencodez/codex-responses/protocol" import { SystemPrompt } from "./system" -import { Usage } from "@opencode-ai/llm" import type { TaskPromptOps } from "@/tool/task" -import { SessionModelContext } from "./model-context" import { LLMRequestPrep } from "./llm/request" import { Instruction } from "./instruction" import { Permission } from "@/permission" import { ToolRegistry } from "@/tool/registry" import { MCP } from "@/mcp" import { Truncate } from "@/tool/truncate" -import { errorMessage } from "@/util/error" -import { isRecord } from "@/util/record" export const Event = SessionCompactionEvent @@ -49,9 +45,6 @@ const TOOL_OUTPUT_MAX_CHARS = 2_000 const PRUNE_PROTECTED_TOOLS = ["skill"] const MIN_PRESERVE_RECENT_TOKENS = 2_000 const MAX_PRESERVE_RECENT_TOKENS = 15_000 -// Keep one overall operation deadline above the bounded transport retry budget. -// Each streamed attempt still uses the WebSocket pool's five-minute idle limit. -const REMOTE_COMPACTION_TIMEOUT_MS = 20 * 60_000 type Turn = { start: number end: number @@ -71,35 +64,6 @@ type CompletedCompaction = { type Phase = NonNullable type ModelRef = { providerID: ProviderV2.ID; modelID: ModelV2.ID } -type RemoteTransition = { - sourceModel: ModelRef - targetCompHash?: string - reason: "comp_hash_changed" | "model_downshift" -} - -function modelContextWindow(model: Provider.Model, profile: CodexResponsesCatalog.Profile | undefined) { - const limits = [model.limit.input, model.limit.context, profile?.contextWindow].filter( - (value): value is number => typeof value === "number" && value > 0, - ) - return limits.length > 0 ? Math.min(...limits) : undefined -} - -function historicalReasoningTokens(messages: SessionV1.WithParts[], turnID: MessageID) { - const boundary = messages.findIndex((message) => message.info.role === "user" && message.info.id === turnID) - if (boundary < 0) return 0 - return messages.slice(0, boundary).reduce((total, message) => { - return ( - total + - message.parts.reduce((subtotal, part) => { - if (part.type !== "reasoning" || !isRecord(part.metadata) || !isRecord(part.metadata.openai)) return subtotal - const encrypted = part.metadata.openai.reasoningEncryptedContent - if (typeof encrypted !== "string") return subtotal - const bytes = Math.max(0, Math.floor((encrypted.length * 3) / 4) - 650) - return subtotal + Math.ceil(bytes / 4) - }, 0) - ) - }, 0) -} const truncate = (value: string) => value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]` @@ -232,7 +196,7 @@ export interface Interface { messages: SessionV1.WithParts[] model: Provider.Model tokens?: SessionV1.Assistant["tokens"] - }) => Effect.Effect + }) => Effect.Effect readonly recordRemoteTurn: (input: { sessionID: SessionID turnID: MessageID @@ -262,7 +226,7 @@ export interface Interface { phase?: Phase turnID?: MessageID overflow?: boolean - transition?: RemoteTransition + transition?: CodexResponsesSessionCompaction.RemoteTransition }) => Effect.Effect readonly capturePending: (input: { sessionID: SessionID; messages: SessionV1.WithParts[] }) => Effect.Effect readonly releasePending: (input: { sessionID: SessionID }) => Effect.Effect @@ -290,275 +254,27 @@ const layer = Layer.effect( const registry = yield* ToolRegistry.Service const mcp = yield* MCP.Service const truncate = yield* Truncate.Service - - const isOverflow = Effect.fn("SessionCompaction.isOverflow")(function* (input: { - tokens: SessionV1.Assistant["tokens"] - model: Provider.Model - additionalTokens?: number - turn?: CodexResponsesCompaction.TurnSettings - profile?: CodexResponsesCatalog.Profile - serverReasoningIncluded?: boolean - messages?: SessionV1.WithParts[] - turnID?: MessageID - }) { - const cfg = yield* config.get() - const authInfo = yield* auth.get(input.model.providerID).pipe(Effect.orDie) - const codexResponses = LLMRequestPrep.isCodexResponses({ - providerID: input.model.providerID, - modelNpm: input.model.api.npm, - authType: authInfo?.type, - config: cfg, - }) - let limit: number | undefined - if (codexResponses && authInfo?.type === "oauth") { - const profile = - input.profile?.modelID === input.model.api.id - ? input.profile - : input.turn?.profile?.modelID === input.model.api.id - ? input.turn.profile - : CodexResponsesCatalog.resolve( - input.model, - CodexResponsesProtocol.accountKey(authInfo.accountId, authInfo.access), - ) - limit = OpenCodezSettings.responsesCompactionLimit( - cfg, - input.model.limit, - profile?.autoCompactTokenLimit ?? profile?.contextWindow, - ) - } - return overflow({ - cfg, - tokens: input.tokens, - model: input.model, - outputTokenMax: flags.outputTokenMax, - limit, - additionalTokens: - (input.additionalTokens ?? 0) + - (codexResponses && input.serverReasoningIncluded !== true && input.messages && input.turnID - ? historicalReasoningTokens(input.messages, input.turnID) - : 0), - }) - }) - - const saveTurnSettings = Effect.fnUntraced(function* (input: { - sessionID: SessionID - turnID: MessageID - model: ModelRef - apiModelID?: string - compHash?: string - accountKey?: string - profile?: CodexResponsesCatalog.Profile - serverReasoningIncluded?: boolean - }) { - const info = yield* session.get(input.sessionID).pipe(Effect.orDie) - const metadata = CodexResponsesCompaction.withTurnSettings(info.metadata, { - turnID: input.turnID, - model: { ...input.model, apiModelID: input.apiModelID }, - compHash: input.compHash, - accountKey: input.accountKey, - profile: input.profile, - serverReasoningIncluded: input.serverReasoningIncluded, - }) - if (metadata === info.metadata) return - yield* session.setMetadata({ sessionID: input.sessionID, metadata }) - }) - - const recordRemoteTurn = Effect.fn("SessionCompaction.recordRemoteTurn")(function* (input: { - sessionID: SessionID - turnID: MessageID - model: Provider.Model - }) { - if (input.model.providerID !== "openai") return undefined - const [cfg, authInfo] = yield* Effect.all([config.get(), auth.get(input.model.providerID).pipe(Effect.orDie)], { - concurrency: "unbounded", - }) - if (authInfo?.type !== "oauth") return undefined - if ( - !LLMRequestPrep.isCodexResponses({ - providerID: input.model.providerID, - modelNpm: input.model.api.npm, - authType: authInfo?.type, - config: cfg, - }) - ) - return undefined - const accountKey = CodexResponsesProtocol.accountKey(authInfo.accountId, authInfo.access) - if (!accountKey) return undefined - const info = yield* session.get(input.sessionID).pipe(Effect.orDie) - const journal = CodexResponsesCompaction.turnSettings(info.metadata) - const existing = journal.find((item) => item.turnID === input.turnID) - if (existing?.accountKey && existing.accountKey !== accountKey) { - throw new Error( - "The ChatGPT login changed during the active turn. Retry with a new message to continue safely.", - ) - } - if (existing?.profile) return existing - yield* Effect.promise(() => CodexResponsesCatalog.settleRefresh(accountKey)) - const profile = CodexResponsesCatalog.resolve(input.model, accountKey) - if (existing?.compHash && profile?.compHash && existing.compHash !== profile.compHash) { - throw new Error( - "The ChatGPT backend changed while resuming the active turn. Retry with a new message to continue safely.", - ) - } - const settings = { - turnID: input.turnID, - model: { - providerID: input.model.providerID, - modelID: input.model.id, - apiModelID: input.model.api.id, - }, - compHash: existing?.compHash ?? profile?.compHash, - accountKey, - profile, - serverReasoningIncluded: existing?.serverReasoningIncluded, - } satisfies CodexResponsesCompaction.TurnSettings - yield* saveTurnSettings({ - sessionID: input.sessionID, - turnID: input.turnID, - model: { providerID: input.model.providerID, modelID: input.model.id }, - apiModelID: input.model.api.id, - compHash: settings.compHash, - accountKey, - profile, - serverReasoningIncluded: settings.serverReasoningIncluded, - }) - return settings - }) - - const saveRemoteTurn = Effect.fn("SessionCompaction.saveRemoteTurn")(function* (input: { - sessionID: SessionID - settings: CodexResponsesCompaction.TurnSettings - }) { - yield* saveTurnSettings({ - sessionID: input.sessionID, - turnID: MessageID.make(input.settings.turnID), - model: { - providerID: ProviderV2.ID.make(input.settings.model.providerID), - modelID: ModelV2.ID.make(input.settings.model.modelID), - }, - apiModelID: input.settings.model.apiModelID, - compHash: input.settings.compHash, - accountKey: input.settings.accountKey, - profile: input.settings.profile, - serverReasoningIncluded: input.settings.serverReasoningIncluded, - }) - }) - - const remoteTransition = Effect.fn("SessionCompaction.remoteTransition")(function* (input: { - sessionID: SessionID - turnID: MessageID - messages: SessionV1.WithParts[] - model: Provider.Model - tokens?: SessionV1.Assistant["tokens"] - }) { - if (input.model.providerID !== "openai") return undefined - const [cfg, authInfo] = yield* Effect.all([config.get(), auth.get(input.model.providerID).pipe(Effect.orDie)], { - concurrency: "unbounded", - }) - if (authInfo?.type !== "oauth") return undefined - if ( - !LLMRequestPrep.isCodexResponses({ - providerID: input.model.providerID, - modelNpm: input.model.api.npm, - authType: authInfo?.type, - config: cfg, - }) - ) - return undefined - const accountKey = CodexResponsesProtocol.accountKey(authInfo.accountId, authInfo.access) - if (!accountKey) return undefined - yield* Effect.promise(() => CodexResponsesCatalog.settleRefresh(accountKey)) - const profile = CodexResponsesCatalog.resolve(input.model, accountKey) - if (!profile) return undefined - const info = yield* session.get(input.sessionID).pipe(Effect.orDie) - const settings = CodexResponsesCompaction.turnSettings(info.metadata) - if (settings.some((item) => item.turnID === input.turnID)) return undefined - - const currentIndex = input.messages.findLastIndex((message) => message.info.id === input.turnID) - if (currentIndex < 0) return undefined - const previousUser = input.messages - .slice(0, currentIndex) - .findLast( - (message): message is SessionV1.WithParts & { info: SessionV1.User } => - message.info.role === "user" && !message.parts.some((part) => part.type === "compaction"), - ) - const exact = previousUser ? settings.findLast((item) => item.turnID === previousUser.info.id) : undefined - const knownTurnIDs = new Set(input.messages.map((message) => message.info.id)) - const forked = settings.length > 0 && settings.every((item) => !knownTurnIDs.has(item.turnID)) - const previous = - exact ?? - (forked && previousUser - ? settings.findLast( - (item) => - item.model.providerID === previousUser.info.model.providerID && - item.model.modelID === previousUser.info.model.modelID, - ) - : undefined) - const previousRef = previous - ? { - providerID: ProviderV2.ID.make(previous.model.providerID), - modelID: ModelV2.ID.make(previous.model.modelID), - } - : previousUser?.info.model.providerID === "openai" - ? { - providerID: previousUser.info.model.providerID, - modelID: previousUser.info.model.modelID, - } - : undefined - const previousModel = previousRef - ? yield* provider.getModel(previousRef.providerID, previousRef.modelID).pipe(Effect.option) - : Option.none() - const previousCompHash = - previous?.compHash ?? - (Option.isSome(previousModel) - ? CodexResponsesCatalog.resolve(previousModel.value, accountKey)?.compHash - : undefined) - if (previousCompHash && profile.compHash && previousCompHash !== profile.compHash) { - return { - sourceModel: Option.isSome(previousModel) - ? { providerID: previousModel.value.providerID, modelID: previousModel.value.id } - : { providerID: input.model.providerID, modelID: input.model.id }, - targetCompHash: profile.compHash, - reason: "comp_hash_changed", - } satisfies RemoteTransition - } - - if (Option.isSome(previousModel) && input.tokens && previousModel.value.api.id !== input.model.api.id) { - const previousProfile = - previous?.profile?.modelID === previousModel.value.api.id - ? previous.profile - : CodexResponsesCatalog.resolve(previousModel.value, accountKey) - const previousWindow = modelContextWindow(previousModel.value, previousProfile) - const currentWindow = modelContextWindow(input.model, profile) - if ( - previousWindow !== undefined && - currentWindow !== undefined && - previousWindow > currentWindow && - (yield* isOverflow({ - tokens: input.tokens, - model: input.model, - profile, - serverReasoningIncluded: previous?.serverReasoningIncluded, - messages: input.messages, - turnID: input.turnID, - })) - ) { - return { - sourceModel: { providerID: previousModel.value.providerID, modelID: previousModel.value.id }, - targetCompHash: profile.compHash, - reason: "model_downshift", - } satisfies RemoteTransition - } - } - - const context = CodexResponsesCompaction.latest(input.messages) - if (!context || !profile.compHash || context.compHash === profile.compHash) return undefined - const sourceModel = - Option.isSome(previousModel) && context.modelID === previousModel.value.api.id - ? { providerID: previousModel.value.providerID, modelID: previousModel.value.id } - : { providerID: input.model.providerID, modelID: input.model.id } - return { sourceModel, targetCompHash: profile.compHash, reason: "comp_hash_changed" } satisfies RemoteTransition + const remoteCompaction = CodexResponsesSessionCompaction.make({ + session, + agents, + plugin, + provider, + events, + flags, + instruction, + system, + permission, + registry, + mcp, + truncate, + config, + auth, }) + const isOverflow = remoteCompaction.isOverflow + const saveTurnSettings = remoteCompaction.saveTurnSettings + const recordRemoteTurn = remoteCompaction.recordRemoteTurn + const saveRemoteTurn = remoteCompaction.saveRemoteTurn + const remoteTransition = remoteCompaction.remoteTransition const estimate = Effect.fn("SessionCompaction.estimate")(function* (input: { messages: SessionV1.WithParts[] @@ -697,24 +413,6 @@ const layer = Layer.effect( return replay }) - const pendingUsers = Effect.fnUntraced(function* (input: { - sessionID: SessionID - turnID: MessageID - markerID: MessageID - }) { - const messages = yield* session.messages({ sessionID: input.sessionID }).pipe(Effect.orDie) - const turnIndex = messages.findIndex((message) => message.info.id === input.turnID) - if (turnIndex < 0) return [] - return messages - .slice(turnIndex + 1) - .filter( - (message): message is SessionV1.WithParts & { info: SessionV1.User } => - message.info.role === "user" && - message.info.id !== input.markerID && - !message.parts.some((part) => part.type === "compaction"), - ) - }) - const completeCompaction = Effect.fn("SessionCompaction.complete")(function* (input: { sessionID: SessionID userMessage: SessionV1.User @@ -928,251 +626,41 @@ const layer = Layer.effect( turnSettings?.profile?.modelID === targetModel.api.id ? turnSettings.profile : accountKey - ? CodexResponsesCatalog.resolve(targetModel, accountKey) - : undefined - if (remote) { - if (!accountKey) throw new Error("OpenAI remote compaction requires a verified ChatGPT account identity") - if (!compactionPart) throw new Error(`Missing compaction part for ${input.parentID}`) - if (!input.promptOps) throw new Error(`Missing prompt operations for remote compaction ${input.parentID}`) - const promptOps = input.promptOps - const ctx = yield* InstanceState.context - const msg: SessionV1.Assistant = { - id: MessageID.ascending(), - role: "assistant", - parentID: input.parentID, - sessionID: input.sessionID, - mode: "compaction", - agent: "compaction", - variant: userMessage.model.variant, - summary: true, - path: { - cwd: ctx.directory, - root: ctx.worktree, - }, - cost: 0, - tokens: { - output: 0, - input: 0, - reasoning: 0, - cache: { read: 0, write: 0 }, - }, - modelID: sourceModel.id, - providerID: sourceModel.providerID, - time: { created: Date.now() }, - } - yield* session.updateMessage(msg) - yield* events.publish(MessageV2.Event.Updated, { sessionID: input.sessionID, info: msg }) - - yield* Effect.logInfo("remote compaction", { - "session.id": input.sessionID, - phase: phase ?? "manual", - messages: history.length, - }) - const compacted = yield* Effect.gen(function* () { - const requestUser = - input.messages.find( - (message): message is SessionV1.WithParts & { info: SessionV1.User } => - message.info.role === "user" && message.info.id === turnID, - )?.info ?? - replay?.info ?? - input.messages.findLast( - (message): message is SessionV1.WithParts & { info: SessionV1.User } => - message.info.role === "user" && !message.parts.some((part) => part.type === "compaction"), - )?.info - if (!requestUser) throw new Error(`Missing request user for compaction ${input.parentID}`) - const requestAgent = yield* agents.get(requestUser.agent) - if (!requestAgent) throw new Error(`Agent not found for compaction: ${requestUser.agent}`) - const sessionInfo = yield* session.get(input.sessionID).pipe(Effect.orDie) - const active = CodexResponsesCompaction.tail(history) - const abort = input.abort ?? AbortSignal.timeout(REMOTE_COMPACTION_TIMEOUT_MS) - const run = (attemptModel: Provider.Model, snapshot?: LLMRequestPrep.Prepared) => - Effect.gen(function* () { - const providerInfo = yield* provider.getProvider(attemptModel.providerID) - const prepared = snapshot - ? yield* Effect.gen(function* () { - if (phase === "mid-turn" && input.overflow && pending.length === 0) return snapshot - const messages = structuredClone(active.messages) - yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages }) - return { - ...snapshot, - messages: yield* MessageV2.toModelMessagesEffect(messages, attemptModel), - } - }) - : yield* Effect.gen(function* () { - const context = yield* SessionModelContext.resolve({ - agent: requestAgent, - model: attemptModel, - session: sessionInfo, - messages: structuredClone(active.messages), - promptOps, - }).pipe( - Effect.provideService(Plugin.Service, plugin), - Effect.provideService(Permission.Service, permission), - Effect.provideService(ToolRegistry.Service, registry), - Effect.provideService(MCP.Service, mcp), - Effect.provideService(Truncate.Service, truncate), - Effect.provideService(RuntimeFlags.Service, flags), - Effect.provideService(Instruction.Service, instruction), - Effect.provideService(SystemPrompt.Service, system), - ) - return yield* LLMRequestPrep.prepare({ - user: requestUser, - turnID: phase === "mid-turn" ? turnID : undefined, - sessionID: input.sessionID, - parentSessionID: sessionInfo.parentID, - sessionMetadata: CodexResponsesCompaction.withMetadata(sessionInfo.metadata, history), - model: attemptModel, - agent: requestAgent, - permission: sessionInfo.permission, - system: context.system, - messages: context.messages, - tools: context.tools, - provider: providerInfo, - auth: authInfo, - plugin, - flags, - isWorkflow: false, - config: cfg, - allowCompHashMismatch: true, - }) - }) - return yield* CodexResponsesCompact.compact({ - model: attemptModel, - provider: providerInfo, - system: prepared.system, - messages: prepared.messages, - tools: prepared.tools, - options: prepared.params.options, - items: active.items, - sessionID: input.sessionID, + ? CodexResponsesCatalog.resolve( + targetModel, accountKey, - turnProfile: - prepared.codexResponsesTurn?.settings.profile?.modelID === attemptModel.api.id - ? prepared.codexResponsesTurn.settings.profile - : undefined, - windowID: previousCompaction?.messageID ?? input.sessionID, - turnID: phase === "mid-turn" ? turnID : undefined, - compaction: { - trigger: input.auto ? "auto" : "manual", - reason: - transition?.reason ?? - (input.auto && - previousCompaction?.compHash && - targetProfile?.compHash && - targetProfile.compHash !== previousCompaction.compHash - ? "comp_hash_changed" - : input.auto - ? "context_limit" - : "user_requested"), - phase: phase === "pre-turn" ? "pre_turn" : phase === "mid-turn" ? "mid_turn" : "standalone_turn", - }, - preserveActiveToolMedia: phase === "mid-turn", - abort, - }).pipe(Effect.timeout(REMOTE_COMPACTION_TIMEOUT_MS)) - }) - const attempt = (attemptModel: Provider.Model, snapshot?: LLMRequestPrep.Prepared) => - run(attemptModel, snapshot).pipe( - Effect.match({ - onFailure: (error) => ({ ok: false as const, error }), - onSuccess: (value) => ({ ok: true as const, value, model: attemptModel }), - }), - ) - const primary = yield* attempt(sourceModel, transition ? undefined : input.prepared) - if (primary.ok) return primary - if ( - !transition || - sourceModel.id === targetModel.id || - !CodexResponsesCompact.canRetryWithCurrentModel(primary.error) - ) - return primary - yield* Effect.logWarning("previous-model remote compaction failed; retrying current model", { - "session.id": input.sessionID, - previousModel: sourceModel.api.id, - currentModel: targetModel.api.id, - error: errorMessage(primary.error), - }) - const fallback = yield* attempt(targetModel) - if (fallback.ok) return fallback - yield* Effect.logError("current-model remote compaction fallback failed", { - "session.id": input.sessionID, - previousModel: sourceModel.api.id, - currentModel: targetModel.api.id, - error: errorMessage(fallback.error), - }) - return primary - }) - - if (!compacted.ok) { - yield* Effect.logError("remote compaction failed", { - "session.id": input.sessionID, - phase: phase ?? "manual", - error: errorMessage(compacted.error), - }) - msg.error = MessageV2.fromError(compacted.error, { providerID: sourceModel.providerID }) - msg.finish = "error" - msg.time.completed = Date.now() - yield* session.updateMessage(msg) - return "stop" - } - if (compacted.value.trimmedOutputs > 0) { - yield* Effect.logInfo("trimmed remote compaction tool outputs", { - "session.id": input.sessionID, - count: compacted.value.trimmedOutputs, - }) - } - - const currentPart = yield* session.getPart({ - sessionID: input.sessionID, - messageID: compactionPart.messageID, - partID: compactionPart.id, - }) - const discoveredPending = turnID - ? yield* pendingUsers({ sessionID: input.sessionID, turnID, markerID: input.parentID }) - : [] - yield* session.updatePart({ - ...compactionPart, - ...(currentPart?.type === "compaction" && currentPart.tail_start_id - ? { tail_start_id: currentPart.tail_start_id } - : discoveredPending[0] - ? { tail_start_id: discoveredPending[0].info.id } - : {}), - remote: { - providerID: "openai", - items: compacted.value.items, - model_id: targetModel.api.id, - comp_hash: transition?.comp_hash ?? targetProfile?.compHash, - }, - }) - const usage = Session.getUsage({ - model: compacted.model, - usage: new Usage({ - totalTokens: compacted.value.usage.total, - inputTokens: compacted.value.usage.input, - outputTokens: compacted.value.usage.output, - reasoningTokens: compacted.value.usage.reasoning, - }), - }) - msg.cost = usage.cost - msg.tokens = usage.tokens - msg.modelID = compacted.model.id - msg.providerID = compacted.model.providerID - msg.finish = "stop" - msg.time.completed = Date.now() - yield* session.updateMessage(msg) - return yield* completeCompaction({ - sessionID: input.sessionID, - userMessage, - replay, - result: "continue", - auto: input.auto, - phase, - direct: phase === "mid-turn", - overflow: input.overflow, - replaceReplayMedia: false, - compactionPart, - }) - } - + undefined, + OpenCodezSettings.responsesContextWindow(cfg), + ) + : undefined + const remoteResult = yield* remoteCompaction.process({ + remote, + sessionID: input.sessionID, + parentID: input.parentID, + messages: input.messages, + history, + userMessage, + compactionPart, + phase, + replay, + pending, + auto: input.auto, + overflow: input.overflow, + abort: input.abort, + prepared: input.prepared, + promptOps: input.promptOps, + sourceModel, + targetModel, + authInfo, + accountKey, + turnID, + targetProfile, + previousCompaction, + transition, + cfg, + complete: completeCompaction, + }) + if (remoteResult) return remoteResult const agent = yield* agents.get("compaction") const model = agent.model ? yield* provider.getModel(agent.model.providerID, agent.model.modelID).pipe(Effect.orDie) @@ -1294,119 +782,9 @@ const layer = Layer.effect( }) }) - const capturePending = Effect.fn("SessionCompaction.capturePending")(function* (input: { - sessionID: SessionID - messages: SessionV1.WithParts[] - }) { - const marker = input.messages.findLast( - (message): message is SessionV1.WithParts & { info: SessionV1.User } => - message.info.role === "user" && - message.parts.some( - (part) => - part.type === "compaction" && - part.phase === "mid-turn" && - part.remote?.providerID === "openai" && - part.turn_id !== undefined, - ), - ) - const part = marker?.parts.find( - (item): item is SessionV1.CompactionPart => - item.type === "compaction" && - item.phase === "mid-turn" && - item.remote?.providerID === "openai" && - item.turn_id !== undefined, - ) - if (!marker || !part?.turn_id || part.tail_start_id) return false - const summary = input.messages.find( - (message) => - message.info.role === "assistant" && - message.info.summary === true && - message.info.parentID === marker.info.id && - message.info.finish !== undefined, - ) - const summaryIndex = summary ? input.messages.findIndex((message) => message.info.id === summary.info.id) : -1 - const continuation = summary - ? input.messages - .slice(summaryIndex + 1) - .find( - (message) => - message.info.role === "assistant" && - message.info.summary !== true && - message.info.parentID === marker.info.id, - ) - : undefined - if (!summary || continuation) return false - const pending = yield* pendingUsers({ - sessionID: input.sessionID, - turnID: part.turn_id, - markerID: marker.info.id, - }) - if (!pending[0]) return false - yield* session.updatePart({ ...part, tail_start_id: pending[0].info.id }) - return true - }) - + const capturePending = remoteCompaction.capturePending const releasePending = Effect.fn("SessionCompaction.releasePending")(function* (input: { sessionID: SessionID }) { - const messages = yield* session.messages({ sessionID: input.sessionID }).pipe(Effect.orDie) - const marker = messages.findLast( - (message): message is SessionV1.WithParts & { info: SessionV1.User } => - message.info.role === "user" && - message.parts.some( - (part) => - part.type === "compaction" && - part.phase === "mid-turn" && - part.remote?.providerID === "openai" && - part.tail_start_id !== undefined, - ), - ) - const part = marker?.parts.find( - (item): item is SessionV1.CompactionPart => - item.type === "compaction" && - item.phase === "mid-turn" && - item.remote?.providerID === "openai" && - item.tail_start_id !== undefined, - ) - if (!marker || !part?.turn_id) return - const summary = messages.find( - (message) => - message.info.role === "assistant" && - message.info.summary === true && - message.info.parentID === marker.info.id && - message.info.finish !== undefined, - ) - if (!summary) return - const summaryIndex = messages.findIndex((message) => message.info.id === summary.info.id) - const continuation = messages - .slice(summaryIndex + 1) - .find( - (message) => - message.info.role === "assistant" && - message.info.summary !== true && - message.info.parentID === marker.info.id && - message.info.time.completed !== undefined, - ) - if (!continuation) return - - const pending = yield* pendingUsers({ - sessionID: input.sessionID, - turnID: part.turn_id, - markerID: marker.info.id, - }) - for (const message of pending) { - yield* replayUser({ sessionID: input.sessionID, message, replaceMedia: false }) - } - const markerIndex = messages.findIndex((message) => message.info.id === marker.info.id) - const afterMarker = new Set(messages.slice(markerIndex + 1).map((message) => message.info.id)) - for (const message of pending) { - if (afterMarker.has(message.info.id)) { - yield* session.removeMessage({ sessionID: input.sessionID, messageID: message.info.id }) - } - } - yield* session.updatePart({ ...part, tail_start_id: undefined }) - yield* Effect.logInfo("released pending input after remote compaction", { - "session.id": input.sessionID, - count: pending.length, - }) + yield* remoteCompaction.releasePending({ ...input, replayUser }) }) const create = Effect.fn("SessionCompaction.create")(function* (input: { @@ -1417,7 +795,7 @@ const layer = Layer.effect( phase?: Phase turnID?: MessageID overflow?: boolean - transition?: RemoteTransition + transition?: CodexResponsesSessionCompaction.RemoteTransition }) { const source = input.turnID ? Option.getOrUndefined( diff --git a/packages/opencode/src/session/llm/ai-sdk.ts b/packages/opencode/src/session/llm/ai-sdk.ts index 8db8985d7b..13d427aab6 100644 --- a/packages/opencode/src/session/llm/ai-sdk.ts +++ b/packages/opencode/src/session/llm/ai-sdk.ts @@ -2,6 +2,7 @@ import { FinishReason, LLMEvent, ProviderMetadata, ToolResultValue } from "@open import { Effect, Schema } from "effect" import { type streamText } from "ai" import { errorMessage } from "@/util/error" +import { ProviderError } from "@/provider/error" type Result = Awaited> type AISDKEvent = Result["fullStream"] extends AsyncIterable ? T : never @@ -85,6 +86,8 @@ export function toLLMEvents( return Effect.succeed([LLMEvent.stepStart({ index: state.step })]) case "finish-step": + if (event.rawFinishReason === "network_error") + return Effect.fail(new ProviderError.ResponseStreamError("Provider finish_reason: network_error")) return Effect.sync(() => { const original = providerMetadata(event.providerMetadata) const metadata = diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index 8986b33816..19d5321b24 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -108,7 +108,13 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre const hasRemoteCompaction = CodexResponsesCompaction.has(input.sessionMetadata) const profile = codexResponses || hasRemoteCompaction - ? (turnProfile ?? CodexResponsesCatalog.resolve(input.model, accountKey)) + ? (turnProfile ?? + CodexResponsesCatalog.resolve( + input.model, + accountKey, + undefined, + OpenCodezSettings.responsesContextWindow(input.config), + )) : undefined if (hasRemoteCompaction && !isOpenaiOauth) { return yield* Effect.fail( @@ -124,20 +130,28 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre const compactionHandoff = isOpenaiOauth && !input.allowCompHashMismatch && CodexResponsesCompaction.handoff(input.sessionMetadata) const opencodezPrompts = OpenCodezIdentity.enabled - ? yield* Effect.promise(async () => { - const opencodez = OpenCodezSession.effective({ - config: input.config, - model: input.model, - modelID: input.model.id, - sessionID: input.sessionID, - metadata: input.sessionMetadata, - }) - return { - systemDisabled: opencodez.systemManual && !opencodez.system, - system: opencodez.system - ? await OpenCodezPromptLibrary.readPrompt(opencodez.system).catch(() => undefined) - : undefined, - } + ? yield* Effect.tryPromise({ + try: async () => { + const opencodez = OpenCodezSession.effective({ + config: input.config, + model: input.model, + modelID: input.model.id, + sessionID: input.sessionID, + metadata: input.sessionMetadata, + }) + const system = opencodez.system ? await OpenCodezPromptLibrary.readPrompt(opencodez.system) : undefined + if (opencodez.system && system === undefined) { + throw new Error(`Selected System prompt is unavailable: ${opencodez.system}`) + } + return { + systemDisabled: opencodez.systemManual && !opencodez.system, + system, + } + }, + catch: (error) => + new Error( + `Unable to load the selected System prompt: ${error instanceof Error ? error.message : String(error)}`, + ), }) : { systemDisabled: false, @@ -319,9 +333,9 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre ), } : {}), - ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), "User-Agent": USER_AGENT, }), + ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), ...input.model.headers, ...headers, ...(isOpenaiOauth ? { [CodexResponsesCapability.HEADER]: codexResponses ? "true" : "false" } : {}), diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 4869e6a234..02a56a716d 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -452,6 +452,20 @@ const layer = Layer.effect( case "step-finish": { const completedSnapshot = yield* snapshot.track() yield* Effect.forEach(Object.keys(ctx.reasoningMap), finishReasoning) + // Anthropic reports thinking blocks it removed before the model saw the + // prompt. Prefix mismatches mean opencode changed history behind a signed + // block; log them so the churn can be tracked down. + const dropped = isRecord(value.providerMetadata?.anthropic) + ? value.providerMetadata.anthropic.inputTransformations + : undefined + if (Array.isArray(dropped) && dropped.length > 0) { + yield* Effect.logWarning("thinking blocks dropped by provider", { + sessionID: ctx.sessionID, + messageID: ctx.assistantMessage.id, + model: ctx.model.id, + transformations: JSON.stringify(dropped), + }) + } const usage = Session.getUsage({ model: ctx.model, usage: value.usage ?? new Usage({}), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index be9f9395a6..731bdde06d 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1153,8 +1153,8 @@ const layer = Layer.effect( lastAssistant?.finish === "unknown" ? yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID) : undefined - const unknownNeedsFollowUp = unknownModel ? yield* isCodexResponses(unknownModel) : false - const modelNeedsFollowUp = hasToolCalls || lastAssistant?.finish === "tool-calls" || unknownNeedsFollowUp + const modelNeedsFollowUp = + hasToolCalls || lastAssistant?.finish === "tool-calls" || lastAssistant?.finish === "unknown" const directRemoteCompactionPart = lastAssistant?.summary === true && lastAssistant.parentID === lastUser.id && @@ -1227,7 +1227,7 @@ const layer = Layer.effect( continue } - const codexResponses = unknownModel ? unknownNeedsFollowUp : yield* isCodexResponses(model) + const codexResponses = yield* isCodexResponses(model) const activeTurnID = codexResponses || directRemoteCompaction ? candidateTurnID : lastUser.id const remoteTransition = yield* compaction.remoteTransition({ diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 17a1841d7f..897db3fceb 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -33,9 +33,10 @@ const RETRYABLE_MESSAGE_PATTERNS = [ /429|500|502|503|504|524/i, /rate increased too quickly|rate limit|rate-limit|rate_limit|too many requests/i, /overloaded|service unavailable|service_unavailable|service-unavailable|internal error|internal_error|internal server error|server error|server_error|server-error|provider returned error|provider_returned_error|provider-returned-error/i, - /terminated|fetch failed|failed to fetch|network error|upstream connect|connection error|connection refused|connection lost|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again|econnrefused|econnreset|etimedout/i, + /terminated|fetch failed|failed to fetch|network[-_\s]error|upstream connect|connection error|connection refused|connection lost|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again|econnrefused|econnreset|etimedout/i, /^timeout$|\b(?:request|response|connection|network|stream|read) (?:timeout|timed out|time out)\b/i, /try your request again|retry your request|resource exhausted|resource_exhausted/i, + /\btry again (?:later|in\b)|\b(?:currently|temporarily) at capacity\b/i, ] function cap(ms: number) { @@ -101,7 +102,7 @@ export function retryable(error: Err, provider: string) { reason: "free_tier_limit", provider, title: "Free limit reached", - message: "Subscribe to OpenCode Go for reliable access to the best open-source models, starting at $5/month.", + message: "Subscribe to OpenCode Go for reliable access to the best open-source models for $10/month.", label: "subscribe", link: GO_UPSELL_URL, }, diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index cfe034146c..9a49c14c2d 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -44,6 +44,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { SessionMessage } from "@opencode-ai/schema/session-message" +import { OpenCodezSession } from "@opencode-ai/core/opencodez/session" const parentTitlePrefix = "New session - " const childTitlePrefix = "Child session - " @@ -336,10 +337,8 @@ export function plan(input: { slug: string; time: { created: number } }, instanc } export const getUsage = (input: { model: Provider.Model; usage: Usage; metadata?: ProviderMetadata }) => { - const safe = (value: number) => { - if (!Number.isFinite(value)) return 0 - return Math.max(0, value) - } + const finite = (value: number) => (Number.isFinite(value) ? value : 0) + const safe = (value: number) => Math.max(0, finite(value)) const inputTokens = safe(input.usage.inputTokens ?? 0) const outputTokens = safe(input.usage.outputTokens ?? 0) const reasoningTokens = safe(input.usage.reasoningTokens ?? 0) @@ -393,13 +392,13 @@ export const getUsage = (input: { model: Provider.Model; usage: Usage; metadata? ? new Decimal(totalNanoAiu).div(100_000_000_000).toNumber() : safe( new Decimal(0) - .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)) - .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000)) + .add(new Decimal(tokens.input).mul(finite(costInfo?.input ?? 0)).div(1_000_000)) + .add(new Decimal(tokens.output).mul(finite(costInfo?.output ?? 0)).div(1_000_000)) + .add(new Decimal(tokens.cache.read).mul(finite(costInfo?.cache?.read ?? 0)).div(1_000_000)) + .add(new Decimal(tokens.cache.write).mul(finite(costInfo?.cache?.write ?? 0)).div(1_000_000)) // TODO: update models.dev to have better pricing model, for now: // charge reasoning tokens at the same rate as output tokens - .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)) + .add(new Decimal(tokens.reasoning).mul(finite(costInfo?.output ?? 0)).div(1_000_000)) .toNumber(), ), tokens, @@ -623,6 +622,7 @@ const layer: Layer.Layer< yield* events.publish(SessionV1.Event.Deleted, { sessionID, info: session }) yield* events.remove(sessionID) + OpenCodezSession.clear(sessionID) } catch (error) { yield* Effect.logError("failed to remove session", { sessionID, error }) } diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index 958bb995cd..5cceb0ac49 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -79,7 +79,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { metadata: val.metadata, status: "running", input: args, - time: { start: Date.now() }, + time: match.state.status === "running" ? match.state.time : { start: Date.now() }, }, } }), diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 5a04ec2139..2f3bff49e1 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -30,8 +30,7 @@ const SKILL_PATTERN = "**/SKILL.md" // when the model is asked to touch opencode's own config files gives it the // actual schemas instead of guesses. const CUSTOMIZE_OPENCODE_SKILL_NAME = "customize-opencode" -const CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION = - "Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself." +const CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION = SkillPlugin.CustomizeOpencodeDescription const CUSTOMIZE_OPENCODE_SKILL_BODY = SkillPlugin.CustomizeOpencodeContent export const Info = Schema.Struct({ diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index f9201be8a7..3f89a63974 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -198,7 +198,7 @@ export const ApplyPatchTool = Tool.define( patch: change.diff, additions: change.additions, deletions: change.deletions, - movePath: change.movePath, + ...(change.movePath ? { movePath: change.movePath } : {}), })) // Check permissions if needed diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 15acc757f3..9167cb3ea6 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -56,7 +56,12 @@ import { PermissionV1 } from "@opencode-ai/core/v1/permission" import { McpCatalog } from "@/mcp/catalog" export function webSearchEnabled(providerID: ProviderV2.ID, flags = { exa: false, parallel: false }) { - return providerID === ProviderV2.ID.opencode || flags.exa || flags.parallel + return ( + providerID === ProviderV2.ID.opencode || + providerID === ProviderV2.ID.make("opencode-go") || + flags.exa || + flags.parallel + ) } type TaskDef = Tool.InferDef diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 1384e5d197..d8ca640cfb 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -210,6 +210,17 @@ export const TaskTool = Tool.define( agent: next.name, parts, }) + if (result.info.role === "assistant" && result.info.error) { + const message = + "message" in result.info.error.data && typeof result.info.error.data.message === "string" + ? result.info.error.data.message + : result.info.error.name + return yield* Effect.fail(new Error(`Subagent failed (task_id: ${nextSession.id}): ${message}`)) + } + const failed = result.parts.findLast((item) => item.type === "tool" && item.state.status === "error") + if (failed?.type === "tool" && failed.state.status === "error") { + return yield* Effect.fail(new Error(`Subagent failed (task_id: ${nextSession.id}): ${failed.state.error}`)) + } return result.parts.findLast((item) => item.type === "text")?.text ?? "" }) diff --git a/packages/opencode/test/account/service.test.ts b/packages/opencode/test/account/service.test.ts index 672d549716..802e91233a 100644 --- a/packages/opencode/test/account/service.test.ts +++ b/packages/opencode/test/account/service.test.ts @@ -10,6 +10,7 @@ import { Account } from "../../src/account/account" import { AccessToken, AccountID, + AccountServiceError, AccountTransportError, DeviceCode, Login, @@ -71,18 +72,18 @@ const deviceTokenClient = (body: unknown, status = 400) => const poll = (body: unknown, status = 400) => Account.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(deviceTokenClient(body, status)))) -it.live("login normalizes trailing slashes in the provided server URL", () => +it.live("login resolves origin-rooted verification URLs from servers with base paths", () => Effect.gen(function* () { const seen: Array = [] const client = HttpClient.make((req) => Effect.gen(function* () { seen.push(`${req.method} ${req.url}`) - if (req.url === "https://one.example.com/auth/device/code") { + if (req.url === "https://one.example.com/console/auth/device/code") { return json(req, { device_code: "device-code", user_code: "user-code", - verification_uri_complete: "/device?user_code=user-code", + verification_uri_complete: "/console/device?user_code=user-code", expires_in: 600, interval: 5, }) @@ -92,11 +93,31 @@ it.live("login normalizes trailing slashes in the provided server URL", () => }), ) - const result = yield* Account.use.login("https://one.example.com/").pipe(Effect.provide(live(client))) + const result = yield* Account.use.login("https://one.example.com/console/").pipe(Effect.provide(live(client))) - expect(seen).toEqual(["POST https://one.example.com/auth/device/code"]) - expect(result.server).toBe("https://one.example.com") - expect(result.url).toBe("https://one.example.com/device?user_code=user-code") + expect(seen).toEqual(["POST https://one.example.com/console/auth/device/code"]) + expect(result.server).toBe("https://one.example.com/console") + expect(result.url).toBe("https://one.example.com/console/device?user_code=user-code") + }), +) + +it.live("login rejects malformed device verification URLs", () => + Effect.gen(function* () { + const client = HttpClient.make((req) => + Effect.succeed( + json(req, { + device_code: "device-code", + user_code: "user-code", + verification_uri_complete: "http://[::1", + expires_in: 600, + interval: 5, + }), + ), + ) + + const error = yield* Effect.flip(Account.use.login("https://one.example.com").pipe(Effect.provide(live(client)))) + expect(error).toBeInstanceOf(AccountServiceError) + if (error instanceof AccountServiceError) expect(error.message).toBe("Invalid device verification URL") }), ) diff --git a/packages/opencode/test/cli/account.test.ts b/packages/opencode/test/cli/account.test.ts index d491b77e17..dfce3bffe6 100644 --- a/packages/opencode/test/cli/account.test.ts +++ b/packages/opencode/test/cli/account.test.ts @@ -4,8 +4,8 @@ import stripAnsi from "strip-ansi" import { defaultConsoleUrl, formatAccountLabel, formatOrgLine } from "../../src/cli/cmd/account" describe("console account display", () => { - test("uses console.opencode.ai as the default login URL", () => { - expect(defaultConsoleUrl).toBe("https://console.opencode.ai") + test("uses opencode.ai/console as the default login URL", () => { + expect(defaultConsoleUrl).toBe("https://opencode.ai/console") }) test("includes the account url in account labels", () => { diff --git a/packages/opencode/test/cli/run/run-process.test.ts b/packages/opencode/test/cli/run/run-process.test.ts index bd5847e272..d2d4bae879 100644 --- a/packages/opencode/test/cli/run/run-process.test.ts +++ b/packages/opencode/test/cli/run/run-process.test.ts @@ -82,10 +82,10 @@ describe("opencode run (non-interactive subprocess)", () => { ) // The test provider's SSE error item is interpreted by the SDK as an unknown - // finish, not a fatal provider/session error. Lock that distinction in so it - // is not accidentally used as the failure compatibility oracle. + // finish, not a fatal provider/session error. Unknown finishes should continue + // the prompt loop so a subsequent response can complete the run. cliIt.concurrent( - "unknown stream finish preserves partial output and exits 0", + "unknown stream finish preserves partial output and continues", ({ llm, opencode }) => Effect.gen(function* () { yield* llm.push( @@ -95,9 +95,10 @@ describe("opencode run (non-interactive subprocess)", () => { }), ) yield* llm.fail("upstream provider exploded mid-stream") + yield* llm.text("recovered") const result = yield* opencode.run("trigger midstream error", { timeoutMs: 30_000 }) expect(result.exitCode).toBe(0) - expect(result.stdout).toBe("partial response\n") + expect(result.stdout).toBe("partial response\nrecovered\n") expect(result.stderr).not.toContain("upstream provider exploded mid-stream") }), 60_000, @@ -213,7 +214,7 @@ describe("opencode run (non-interactive subprocess)", () => { ) cliIt.concurrent( - "--format json records partial output for an unknown stream finish", + "--format json records an unknown stream finish and continuation", ({ llm, opencode }) => Effect.gen(function* () { yield* llm.push( @@ -223,6 +224,7 @@ describe("opencode run (non-interactive subprocess)", () => { }), ) yield* llm.fail("provider failed") + yield* llm.text("recovered") const result = yield* opencode.run("fail after output", { format: "json" }) const events = opencode.parseJsonEvents(result.stdout) @@ -234,9 +236,14 @@ describe("opencode run (non-interactive subprocess)", () => { "step_finish", "step_start", "step_finish", + "step_start", + "text", + "step_finish", ]) expect(events[1]?.part).toEqual(expect.objectContaining({ type: "text", text: "partial json" })) - expect(events.at(-1)?.part).toEqual(expect.objectContaining({ type: "step-finish", reason: "unknown" })) + expect(events[5]?.part).toEqual(expect.objectContaining({ type: "step-finish", reason: "unknown" })) + expect(events[7]?.part).toEqual(expect.objectContaining({ type: "text", text: "recovered" })) + expect(events.at(-1)?.part).toEqual(expect.objectContaining({ type: "step-finish", reason: "stop" })) }), 60_000, ) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 8f72c0cb7f..8d5baede50 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -2,12 +2,14 @@ import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { httpClient } from "@opencode-ai/core/effect/app-node-platform" -import { Cause, Effect, Exit, Layer, Option } from "effect" +import { Cause, Effect, Exit, Layer, Logger, Option } from "effect" import { NamedError } from "@opencode-ai/core/util/error" import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http" import { Config } from "@/config/config" import { ConfigManaged } from "@/config/managed" import { ConfigParse } from "../../src/config/parse" +import { ConfigV2Compat } from "../../src/config/v2-compat" +import { snapshot } from "./snapshot" import { Npm } from "@opencode-ai/core/npm" import { InstanceRef } from "../../src/effect/instance-ref" @@ -397,6 +399,191 @@ it.effect("updates global config and omits empty shell key in jsonc", () => ), ) +it.effect("logs global update diagnostics once without exposing values", () => + withGlobalConfig( + { + config: { + providers: { example: { settings: { apiKey: "keep-me" } } }, + }, + }, + ({ dir }) => + Effect.gen(function* () { + const messages: unknown[] = [] + yield* Config.use.updateGlobal({ username: "updated" }).pipe( + Effect.provide( + Logger.layer([ + Logger.make((options) => { + messages.push(options.message) + }), + ]), + ), + ) + expect(JSON.stringify(messages)).not.toContain("keep-me") + expect( + messages.filter((item) => Array.isArray(item) && item[0] === "configuration compatibility diagnostic"), + ).toEqual([ + [ + "configuration compatibility diagnostic", + expect.objectContaining({ + source: path.join(dir, "opencode.json"), + kind: "unsupported", + path: ["providers"], + }), + ], + ]) + }), + ), +) + +const updateFixtures = path.join(import.meta.dir, "fixtures/v2-compat") +const globalInputs = [...new Bun.Glob("update-global/*-input.{json,jsonc}").scanSync({ cwd: updateFixtures })].sort() +const projectInputs = [...new Bun.Glob("update-project/*-input.json").scanSync({ cwd: updateFixtures })].sort() +if (!globalInputs.length || !projectInputs.length) throw new Error("Missing config update fixtures") + +for (const input of globalInputs) { + const extension = path.extname(input) + const name = input.slice(0, -`-input${extension}`.length) + const prefix = path.join(updateFixtures, name) + it.live(`fixture ${name}`, () => + withGlobalConfig({}, ({ dir }) => + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const file = path.join(dir, `opencode${extension}`) + yield* fs.writeFileString(file, yield* fs.readFileString(path.join(updateFixtures, input))) + const patch = ConfigParse.schema(ConfigV1.Info, yield* fs.readJson(`${prefix}-patch.json`), input) + const updated = yield* Config.use.updateGlobal(patch) + const written = yield* fs.readFileString(file) + + yield* Effect.promise(() => snapshot(`${prefix}-output${extension}`, written)) + yield* Effect.promise(() => snapshot(`${prefix}-normalized.json`, JSON.stringify(updated.info, null, 2) + "\n")) + }), + ), + ) +} + +for (const input of projectInputs) { + const name = input.slice(0, -"-input.json".length) + const prefix = path.join(updateFixtures, name) + it.instance(`fixture ${name}`, () => + Effect.gen(function* () { + const instance = yield* TestInstance + const fs = yield* FSUtil.Service + const file = path.join(instance.directory, "config.json") + yield* fs.writeFileString(file, yield* fs.readFileString(path.join(updateFixtures, input))) + const patch = ConfigParse.schema(ConfigV1.Info, yield* fs.readJson(`${prefix}-patch.json`), input) + yield* Config.use.update(patch) + const written = yield* fs.readFileString(file) + const normalized = ConfigParse.schema( + ConfigV1.Info, + ConfigV2Compat.lower(ConfigParse.jsonc(written, file)).value, + file, + ) + + yield* Effect.promise(() => snapshot(`${prefix}-output.json`, written)) + yield* Effect.promise(() => snapshot(`${prefix}-normalized.json`, JSON.stringify(normalized, null, 2) + "\n")) + }), + ) +} + +for (const name of ["opencode.json", "opencode.jsonc"]) { + it.live(`rejects updating ${name} with native permissions without writing it`, () => + withGlobalConfig( + { name, config: { permissions: [{ action: "read", resource: "secret-resource", effect: "deny" }] } }, + ({ dir }) => + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const file = path.join(dir, name) + const before = yield* fs.readFileString(file) + const exit = yield* Effect.exit(Config.use.updateGlobal({ username: "changed" })) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) { + const error = Cause.squash(exit.cause) + expect(error).toMatchObject({ data: { path: file, issues: [{ path: ["permissions"] }] } }) + expect(JSON.stringify(error)).not.toContain("secret-resource") + } + expect(yield* fs.readFileString(file)).toBe(before) + }), + ), + ) +} + +it.instance("rejects a project update with native agent permissions without writing it", () => + Effect.gen(function* () { + const instance = yield* TestInstance + const fs = yield* FSUtil.Service + const file = path.join(instance.directory, "config.json") + const before = JSON.stringify({ agents: { reviewer: { permissions: [] } } }) + yield* fs.writeFileString(file, before) + const exit = yield* Effect.exit(Config.use.update({ username: "changed" })) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) + expect(Cause.squash(exit.cause)).toMatchObject({ + data: { path: file, issues: [{ path: ["agents", "reviewer", "permissions"] }] }, + }) + expect(yield* fs.readFileString(file)).toBe(before) + }), +) + +it.effect("native project MCP servers override inherited V1 disabled state", () => + withConfigTree( + { + global: { + mcp: { + shared: { type: "local", command: ["global-mcp"], enabled: false }, + }, + }, + project: { + mcp: { + servers: { + shared: { type: "local", command: ["project-mcp"] }, + }, + }, + }, + }, + Effect.gen(function* () { + expect((yield* Config.use.get()).mcp?.shared).toMatchObject({ + type: "local", + command: ["project-mcp"], + enabled: true, + }) + }), + ), +) + +it.effect("rejects native project permissions even with inherited V1 rules", () => + withConfigTree( + { + global: { + permission: { read: "deny", bash: "ask" }, + agent: { reviewer: { permission: { edit: "deny" } } }, + }, + project: { + permissions: [{ action: "read", resource: "*", effect: "allow" }], + agents: { + reviewer: { + system: "Review carefully", + permissions: [{ action: "edit", resource: "*", effect: "allow" }], + }, + }, + }, + }, + Effect.gen(function* () { + const exit = yield* Effect.exit(Config.use.get()) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) + expect(Cause.squash(exit.cause)).toMatchObject({ + data: { + path: expect.stringContaining(path.join("project", "opencode.json")), + issues: [ + { path: ["permissions"], message: expect.stringContaining('Use V1 "permission" rules or run opencode2') }, + { path: ["agents", "reviewer", "permissions"], message: expect.stringContaining("not supported") }, + ], + }, + }) + }), + ), +) + it.instance( "loads formatter boolean config", Effect.gen(function* () { diff --git a/packages/opencode/test/config/fixtures/v2-compat/README.md b/packages/opencode/test/config/fixtures/v2-compat/README.md new file mode 100644 index 0000000000..7875647633 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/README.md @@ -0,0 +1,40 @@ +# Config Transformation Fixtures + +Each directory is an operation, with a flat list of files grouped by case-name prefixes. Add a case without adding another +test body; the runners discover `*-input.*` files in sorted order. + +## Operations + +| Directory | Inputs | Expected outputs | Operation | +| ----------------- | ---------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `read/` | `-input.jsonc` | `-output.json` | Parse JSONC, lower supported V2 fields, and decode the V1 schema. | +| `update-global/` | `-input.json` or `.jsonc`, `-patch.json` | `-output.json` or `.jsonc`, `-normalized.json` | Write an isolated global file and invoke the real `Config.updateGlobal` service. | +| `update-project/` | `-input.json`, `-patch.json` | `-output.json`, `-normalized.json` | Write an isolated project `config.json` and invoke the real `Config.update` service. | + +Read outputs capture the complete decoded document, including V1 schema defaults, but not environment-dependent runtime +defaults such as the OS username. The read runner also checks that lowering did not mutate its input. + +For updates, `-output.*` is the exact text written by the service, including comments, formatting, and the presence or +absence of a final newline. `-normalized.json` records the V1 config returned by `updateGlobal`, or the decoded saved file +for project updates (which return no config). Native V2 data can remain in the saved file even when it is absent or has a +different shape in the in-memory V1 output. + +These are checked-in expectations, not output generated during ordinary test runs. Missing expectations fail the test. +Focused tests separately cover invalid inputs, diagnostics, secret redaction, logging, and cross-source behavior. + +## Run + +From `packages/opencode`: + +```sh +bun test test/config/v2-compat.test.ts test/config/config.test.ts --timeout 30000 +``` + +To intentionally regenerate expected outputs: + +```sh +UPDATE_CONFIG_FIXTURES=1 bun test test/config/v2-compat.test.ts test/config/config.test.ts --timeout 30000 +``` + +Review every changed `*-output.*` and `*-normalized.json` before accepting it. Do not run a formatter on update outputs; their +exact formatting is part of the snapshot. Inputs are authored by hand and are never rewritten by the fixture runner. diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/agents-commands-precedence-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/agents-commands-precedence-input.jsonc new file mode 100644 index 0000000000..7315106cdc --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/agents-commands-precedence-input.jsonc @@ -0,0 +1,17 @@ +{ + "permission": { "bash": "deny", "*": "ask", "edit": "deny" }, + "agent": { + "reviewer": { "prompt": "Legacy prompt", "permission": { "bash": "deny", "edit": "deny" } }, + }, + "agents": { + "reviewer": { + "system": "Native prompt", + }, + "native": {}, + }, + "command": { "review": { "template": "Legacy review" } }, + "commands": { + "review": { "template": "Native review" }, + "modern": { "template": "Native command" }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/agents-commands-precedence-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/agents-commands-precedence-output.json new file mode 100644 index 0000000000..55815600b8 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/agents-commands-precedence-output.json @@ -0,0 +1,29 @@ +{ + "permission": { + "bash": "deny", + "*": "ask", + "edit": "deny" + }, + "agent": { + "reviewer": { + "prompt": "Legacy prompt", + "permission": { + "bash": "deny", + "edit": "deny" + }, + "options": {} + }, + "native": { + "options": {}, + "permission": {} + } + }, + "command": { + "review": { + "template": "Legacy review" + }, + "modern": { + "template": "Native command" + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/agents-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/agents-input.jsonc new file mode 100644 index 0000000000..abd03aba24 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/agents-input.jsonc @@ -0,0 +1,15 @@ +{ + "agents": { + "reviewer": { + "model": { "providerID": "anthropic", "model": "claude-sonnet", "variant": "thinking" }, + "system": "Review carefully.", + "description": "Reviews changes", + "mode": "subagent", + "hidden": true, + "color": "#123abc", + "steps": 4, + "disabled": true, + }, + "quick": { "model": "openai/gpt-4.1#fast", "disabled": false }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/agents-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/agents-output.json new file mode 100644 index 0000000000..b94b1f4297 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/agents-output.json @@ -0,0 +1,24 @@ +{ + "agent": { + "reviewer": { + "description": "Reviews changes", + "mode": "subagent", + "hidden": true, + "color": "#123abc", + "steps": 4, + "prompt": "Review carefully.", + "disable": true, + "model": "anthropic/claude-sonnet", + "variant": "thinking", + "options": {}, + "permission": {} + }, + "quick": { + "disable": false, + "model": "openai/gpt-4.1", + "variant": "fast", + "options": {}, + "permission": {} + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/commands-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/commands-input.jsonc new file mode 100644 index 0000000000..88e433e6d5 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/commands-input.jsonc @@ -0,0 +1,12 @@ +{ + "commands": { + "review": { + "template": "Review $ARGUMENTS", + "description": "Review code", + "agent": "reviewer", + "model": { "providerID": "anthropic", "model": "claude-sonnet", "variant": "thinking" }, + "subtask": true, + }, + "quick": { "template": "Quick review", "model": "openai/gpt-4.1#fast" }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/commands-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/commands-output.json new file mode 100644 index 0000000000..9365d5999f --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/commands-output.json @@ -0,0 +1,17 @@ +{ + "command": { + "review": { + "template": "Review $ARGUMENTS", + "description": "Review code", + "agent": "reviewer", + "model": "anthropic/claude-sonnet", + "subtask": true, + "variant": "thinking" + }, + "quick": { + "template": "Quick review", + "model": "openai/gpt-4.1", + "variant": "fast" + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/ignored-fields-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/ignored-fields-input.jsonc new file mode 100644 index 0000000000..1f054f1cf2 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/ignored-fields-input.jsonc @@ -0,0 +1,8 @@ +{ + "plugins": [{ "package": "@example/native-plugin" }], + "providers": { "native": { "models": { "example": { "name": "Native model" } } } }, + "policies": [{ "action": "provider.use", "effect": "deny", "resource": "openai" }], + "websearch": "native-only", + "warming": true, + "experimental": { "portable_shell_scanner": true }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/ignored-fields-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/ignored-fields-output.json new file mode 100644 index 0000000000..f1d962881b --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/ignored-fields-output.json @@ -0,0 +1,3 @@ +{ + "experimental": {} +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/lsp-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/lsp-input.jsonc new file mode 100644 index 0000000000..33f94f2be1 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/lsp-input.jsonc @@ -0,0 +1,8 @@ +{ + "lsp": { + "typescript": { "command": ["typescript-language-server", "--stdio"] }, + "compatible": { "command": ["custom-lsp"], "extensions": [".custom"] }, + "incompatible": { "command": ["incompatible-lsp"] }, + "disabled": { "disabled": true }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/lsp-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/lsp-output.json new file mode 100644 index 0000000000..0f21393316 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/lsp-output.json @@ -0,0 +1,14 @@ +{ + "lsp": { + "typescript": { + "command": ["typescript-language-server", "--stdio"] + }, + "compatible": { + "command": ["custom-lsp"], + "extensions": [".custom"] + }, + "disabled": { + "disabled": true + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-enablement-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-enablement-input.jsonc new file mode 100644 index 0000000000..c0f2b0da3e --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-enablement-input.jsonc @@ -0,0 +1,15 @@ +{ + "mcp": { + "legacy-disabled": { "enabled": false }, + "legacy-enabled": { "enabled": true }, + "existing": { "type": "local", "command": ["existing-mcp"], "enabled": true }, + "flat-disabled": { "type": "local", "command": ["legacy"], "enabled": false, "codemode": false }, + "flat-enabled": { "type": "local", "command": ["legacy"], "enabled": true, "disabled": true }, + "servers": { + "type": { "type": "local", "command": ["type-mcp"] }, + "enabled": { "type": "remote", "url": "https://example.com/mcp" }, + "explicit-enabled": { "type": "local", "command": ["enabled-mcp"], "disabled": false }, + "explicit-disabled": { "type": "local", "command": ["disabled-mcp"], "disabled": true }, + }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-enablement-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-enablement-output.json new file mode 100644 index 0000000000..4ae15ca1fb --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-enablement-output.json @@ -0,0 +1,45 @@ +{ + "mcp": { + "legacy-disabled": { + "enabled": false + }, + "legacy-enabled": { + "enabled": true + }, + "existing": { + "type": "local", + "command": ["existing-mcp"], + "enabled": true + }, + "flat-disabled": { + "type": "local", + "command": ["legacy"], + "enabled": false + }, + "flat-enabled": { + "type": "local", + "command": ["legacy"], + "enabled": true + }, + "type": { + "type": "local", + "command": ["type-mcp"], + "enabled": true + }, + "enabled": { + "type": "remote", + "url": "https://example.com/mcp", + "enabled": true + }, + "explicit-enabled": { + "type": "local", + "command": ["enabled-mcp"], + "enabled": true + }, + "explicit-disabled": { + "type": "local", + "command": ["disabled-mcp"], + "enabled": false + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-merge-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-merge-input.jsonc new file mode 100644 index 0000000000..a97d2aedc0 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-merge-input.jsonc @@ -0,0 +1,11 @@ +{ + // Flat V1 entries win when an enveloped V2 server has the same name. + "mcp": { + "legacy": { "type": "local", "command": ["legacy-mcp"], "enabled": false }, + "shared": { "type": "remote", "url": "https://legacy.example.com/mcp" }, + "servers": { + "shared": { "type": "remote", "url": "https://native.example.com/mcp", "disabled": false }, + "native": { "type": "local", "command": ["native-mcp"], "disabled": true }, + }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-merge-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-merge-output.json new file mode 100644 index 0000000000..8ddce438bf --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-merge-output.json @@ -0,0 +1,18 @@ +{ + "mcp": { + "legacy": { + "type": "local", + "command": ["legacy-mcp"], + "enabled": false + }, + "shared": { + "type": "remote", + "url": "https://legacy.example.com/mcp" + }, + "native": { + "type": "local", + "command": ["native-mcp"], + "enabled": false + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-oauth-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-oauth-input.jsonc new file mode 100644 index 0000000000..bed851abb6 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-oauth-input.jsonc @@ -0,0 +1,19 @@ +{ + "mcp": { + "servers": { + "authenticated": { + "type": "remote", + "url": "https://oauth.example.com/mcp", + "headers": { "Authorization": "Bearer token" }, + "oauth": { + "client_id": "client", + "client_secret": "secret", + "scope": "read write", + "callback_port": 19877, + "redirect_uri": "http://127.0.0.1:19877/callback", + }, + }, + "anonymous": { "type": "remote", "url": "https://anonymous.example.com/mcp", "oauth": false }, + }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-oauth-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-oauth-output.json new file mode 100644 index 0000000000..91ed4f1d69 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-oauth-output.json @@ -0,0 +1,25 @@ +{ + "mcp": { + "authenticated": { + "type": "remote", + "url": "https://oauth.example.com/mcp", + "headers": { + "Authorization": "Bearer token" + }, + "oauth": { + "clientId": "client", + "clientSecret": "secret", + "scope": "read write", + "callbackPort": 19877, + "redirectUri": "http://127.0.0.1:19877/callback" + }, + "enabled": true + }, + "anonymous": { + "type": "remote", + "url": "https://anonymous.example.com/mcp", + "oauth": false, + "enabled": true + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-partial-timeout-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-partial-timeout-input.jsonc new file mode 100644 index 0000000000..001199938d --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-partial-timeout-input.jsonc @@ -0,0 +1,3 @@ +{ + "mcp": { "timeout": { "startup": 1000, "catalog": 2000 } }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-partial-timeout-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-partial-timeout-output.json new file mode 100644 index 0000000000..b50b419d08 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-partial-timeout-output.json @@ -0,0 +1,3 @@ +{ + "mcp": {} +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-enabled-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-enabled-input.jsonc new file mode 100644 index 0000000000..02285ad6d1 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-enabled-input.jsonc @@ -0,0 +1,7 @@ +{ + // An object-valued type must not hide a flat enabled-only server. + "mcp": { + "servers": { "type": {}, "enabled": false }, + "timeout": { "enabled": true }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-enabled-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-enabled-output.json new file mode 100644 index 0000000000..004e72494b --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-enabled-output.json @@ -0,0 +1,10 @@ +{ + "mcp": { + "servers": { + "enabled": false + }, + "timeout": { + "enabled": true + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-input.jsonc new file mode 100644 index 0000000000..df1af73c0e --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-input.jsonc @@ -0,0 +1,6 @@ +{ + "mcp": { + "servers": { "type": "local", "command": ["server-named-servers"] }, + "timeout": { "type": "remote", "url": "https://timeout.example.com/mcp" }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-output.json new file mode 100644 index 0000000000..6e69bb676f --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-reserved-output.json @@ -0,0 +1,12 @@ +{ + "mcp": { + "servers": { + "type": "local", + "command": ["server-named-servers"] + }, + "timeout": { + "type": "remote", + "url": "https://timeout.example.com/mcp" + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-timeouts-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-timeouts-input.jsonc new file mode 100644 index 0000000000..44541bbe24 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-timeouts-input.jsonc @@ -0,0 +1,15 @@ +{ + "mcp": { + "timeout": { "catalog": 8000, "execution": 8000 }, + "servers": { + "safe": { "type": "local", "command": ["safe-mcp"], "timeout": { "catalog": 3000, "execution": 3000 } }, + "unsafe": { "type": "local", "command": ["unsafe-mcp"], "timeout": { "catalog": 2000, "execution": 4000 } }, + "partial": { "type": "local", "command": ["partial-mcp"], "timeout": { "execution": 5000 } }, + "startup": { + "type": "local", + "command": ["startup-mcp"], + "timeout": { "startup": 1000, "catalog": 3000, "execution": 3000 }, + }, + }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/mcp-timeouts-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-timeouts-output.json new file mode 100644 index 0000000000..7f9bc3b0aa --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/mcp-timeouts-output.json @@ -0,0 +1,28 @@ +{ + "mcp": { + "safe": { + "type": "local", + "command": ["safe-mcp"], + "enabled": true, + "timeout": 3000 + }, + "unsafe": { + "type": "local", + "command": ["unsafe-mcp"], + "enabled": true + }, + "partial": { + "type": "local", + "command": ["partial-mcp"], + "enabled": true + }, + "startup": { + "type": "local", + "command": ["startup-mcp"], + "enabled": true + } + }, + "experimental": { + "mcp_timeout": 8000 + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/model-object-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/model-object-input.jsonc new file mode 100644 index 0000000000..1fe39335cb --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/model-object-input.jsonc @@ -0,0 +1,4 @@ +{ + "$schema": "https://opencode.ai/config.json", + "model": { "providerID": "anthropic", "model": "claude-sonnet", "variant": "fast" }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/model-object-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/model-object-output.json new file mode 100644 index 0000000000..05db431121 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/model-object-output.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://opencode.ai/config.json", + "model": "anthropic/claude-sonnet" +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/model-string-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/model-string-input.jsonc new file mode 100644 index 0000000000..30d1ab9b86 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/model-string-input.jsonc @@ -0,0 +1,4 @@ +{ + "model": "anthropic/claude-sonnet", + "permission": "deny", +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/model-string-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/model-string-output.json new file mode 100644 index 0000000000..f8cc9263c0 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/model-string-output.json @@ -0,0 +1,6 @@ +{ + "model": "anthropic/claude-sonnet", + "permission": { + "*": "deny" + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/model-variant-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/model-variant-input.jsonc new file mode 100644 index 0000000000..354a711404 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/model-variant-input.jsonc @@ -0,0 +1,3 @@ +{ + "model": "anthropic/claude-sonnet#fast", +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/model-variant-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/model-variant-output.json new file mode 100644 index 0000000000..6f472abc0b --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/model-variant-output.json @@ -0,0 +1,3 @@ +{ + "model": "anthropic/claude-sonnet" +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/settings-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/settings-input.jsonc new file mode 100644 index 0000000000..bde7a6580f --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/settings-input.jsonc @@ -0,0 +1,12 @@ +{ + "snapshots": false, + "media": { + "image": { "auto_resize": false, "max_width": 1920, "max_height": 1080, "max_base64_bytes": 4096 }, + }, + "compaction": { "auto": false, "keep": { "tokens": 12000 }, "buffer": 2048 }, + "experimental": { + "subagent_depth": 3, + "policies": [{ "effect": "deny", "action": "provider.use", "resource": "openai" }], + "batch_tool": true, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/settings-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/settings-output.json new file mode 100644 index 0000000000..273ec79f34 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/settings-output.json @@ -0,0 +1,27 @@ +{ + "compaction": { + "auto": false, + "preserve_recent_tokens": 12000, + "reserved": 2048 + }, + "experimental": { + "policies": [ + { + "effect": "deny", + "action": "provider.use", + "resource": "openai" + } + ], + "batch_tool": true + }, + "snapshot": false, + "attachment": { + "image": { + "auto_resize": false, + "max_width": 1920, + "max_height": 1080, + "max_base64_bytes": 4096 + } + }, + "subagent_depth": 3 +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/settings-precedence-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/settings-precedence-input.jsonc new file mode 100644 index 0000000000..4f4de052af --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/settings-precedence-input.jsonc @@ -0,0 +1,15 @@ +{ + "snapshot": false, + "snapshots": true, + "attachment": { "image": { "max_width": 640 } }, + "media": { "image": { "max_width": 1920 } }, + "subagent_depth": 1, + "experimental": { "subagent_depth": 3, "mcp_timeout": 4000 }, + "compaction": { + "preserve_recent_tokens": 100, + "keep": { "tokens": 200 }, + "reserved": 300, + "buffer": 400, + }, + "mcp": { "timeout": { "catalog": 8000, "execution": 8000 } }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/settings-precedence-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/settings-precedence-output.json new file mode 100644 index 0000000000..32c1efada4 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/settings-precedence-output.json @@ -0,0 +1,17 @@ +{ + "snapshot": false, + "attachment": { + "image": { + "max_width": 640 + } + }, + "subagent_depth": 1, + "experimental": { + "mcp_timeout": 4000 + }, + "compaction": { + "preserve_recent_tokens": 100, + "reserved": 300 + }, + "mcp": {} +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/skills-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/read/skills-input.jsonc new file mode 100644 index 0000000000..c5156eb03a --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/skills-input.jsonc @@ -0,0 +1,3 @@ +{ + "skills": ["./skills", "https://example.com/skills", "/opt/skills", "http://localhost:8080/skills"], +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/read/skills-output.json b/packages/opencode/test/config/fixtures/v2-compat/read/skills-output.json new file mode 100644 index 0000000000..169bc6fe0a --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/read/skills-output.json @@ -0,0 +1,6 @@ +{ + "skills": { + "paths": ["./skills", "/opt/skills"], + "urls": ["https://example.com/skills", "http://localhost:8080/skills"] + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-input.jsonc new file mode 100644 index 0000000000..0107587203 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-input.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "https://opencode.ai/config.json", + // Empty shell in a global update removes the setting. + "shell": "bash", + "model": { "providerID": "example", "model": "demo" }, + "snapshots": false, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-normalized.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-normalized.json new file mode 100644 index 0000000000..133badb77e --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-normalized.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://opencode.ai/config.json", + "model": "example/demo", + "snapshot": false +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-output.jsonc b/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-output.jsonc new file mode 100644 index 0000000000..a347fd0ac5 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-output.jsonc @@ -0,0 +1,5 @@ +{ + "$schema": "https://opencode.ai/config.json", + "model": { "providerID": "example", "model": "demo" }, + "snapshots": false, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-patch.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-patch.json new file mode 100644 index 0000000000..f448f45324 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/clear-shell-patch.json @@ -0,0 +1,3 @@ +{ + "shell": "" +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-input.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-input.json new file mode 100644 index 0000000000..85438599c2 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-input.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://opencode.ai/config.json", + "username": "before", + "mcp": { + "servers": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "disabled": true + } + } + }, + "providers": { + "example": { + "settings": { "apiKey": "fixture-secret" } + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-normalized.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-normalized.json new file mode 100644 index 0000000000..a2c3189715 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-normalized.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://opencode.ai/config.json", + "username": "after", + "mcp": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "enabled": false + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-output.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-output.json new file mode 100644 index 0000000000..2ac6239a03 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-output.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://opencode.ai/config.json", + "username": "after", + "mcp": { + "servers": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "disabled": true + } + } + }, + "providers": { + "example": { + "settings": { + "apiKey": "fixture-secret" + } + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-patch.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-patch.json new file mode 100644 index 0000000000..1af8720f4b --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-json-patch.json @@ -0,0 +1,3 @@ +{ + "username": "after" +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-input.jsonc b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-input.jsonc new file mode 100644 index 0000000000..db714876d1 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-input.jsonc @@ -0,0 +1,19 @@ +{ + "$schema": "https://opencode.ai/config.json", + // The V1 update must keep comments and native settings. + "username": "before", + "mcp": { + "servers": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "disabled": true, + }, + }, + }, + "providers": { + "example": { + "settings": { "apiKey": "fixture-secret" }, + }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-normalized.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-normalized.json new file mode 100644 index 0000000000..a2c3189715 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-normalized.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://opencode.ai/config.json", + "username": "after", + "mcp": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "enabled": false + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-output.jsonc b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-output.jsonc new file mode 100644 index 0000000000..abd8b673e2 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-output.jsonc @@ -0,0 +1,19 @@ +{ + "$schema": "https://opencode.ai/config.json", + // The V1 update must keep comments and native settings. + "username": "after", + "mcp": { + "servers": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "disabled": true, + }, + }, + }, + "providers": { + "example": { + "settings": { "apiKey": "fixture-secret" }, + }, + }, +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-patch.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-patch.json new file mode 100644 index 0000000000..1af8720f4b --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/preserve-v2-jsonc-patch.json @@ -0,0 +1,3 @@ +{ + "username": "after" +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-input.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-input.json new file mode 100644 index 0000000000..35ec5a1cd7 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-input.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://opencode.ai/config.json", + "snapshots": true, + "agents": { + "reviewer": { "disabled": false } + }, + "mcp": { + "servers": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "disabled": false + } + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-normalized.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-normalized.json new file mode 100644 index 0000000000..07596a743b --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-normalized.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "modern": { + "enabled": false + } + }, + "snapshot": false, + "permission": { + "read": "deny" + }, + "agent": { + "reviewer": { + "disable": true, + "options": {}, + "permission": {} + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-output.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-output.json new file mode 100644 index 0000000000..1edbbdddda --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-output.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://opencode.ai/config.json", + "snapshots": true, + "agents": { + "reviewer": { + "disabled": false + } + }, + "mcp": { + "servers": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "disabled": false + } + }, + "modern": { + "enabled": false + } + }, + "snapshot": false, + "permission": { + "read": "deny" + }, + "agent": { + "reviewer": { + "disable": true, + "options": {}, + "permission": {} + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-patch.json b/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-patch.json new file mode 100644 index 0000000000..ad732d661d --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-global/v1-overrides-patch.json @@ -0,0 +1,6 @@ +{ + "snapshot": false, + "permission": { "read": "deny" }, + "agent": { "reviewer": { "disable": true } }, + "mcp": { "modern": { "enabled": false } } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-input.json b/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-input.json new file mode 100644 index 0000000000..85438599c2 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-input.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://opencode.ai/config.json", + "username": "before", + "mcp": { + "servers": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "disabled": true + } + } + }, + "providers": { + "example": { + "settings": { "apiKey": "fixture-secret" } + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-normalized.json b/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-normalized.json new file mode 100644 index 0000000000..a2c3189715 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-normalized.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://opencode.ai/config.json", + "username": "after", + "mcp": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "enabled": false + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-output.json b/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-output.json new file mode 100644 index 0000000000..2ac6239a03 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-output.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://opencode.ai/config.json", + "username": "after", + "mcp": { + "servers": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "disabled": true + } + } + }, + "providers": { + "example": { + "settings": { + "apiKey": "fixture-secret" + } + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-patch.json b/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-patch.json new file mode 100644 index 0000000000..1af8720f4b --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-project/preserve-v2-patch.json @@ -0,0 +1,3 @@ +{ + "username": "after" +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-input.json b/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-input.json new file mode 100644 index 0000000000..35ec5a1cd7 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-input.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://opencode.ai/config.json", + "snapshots": true, + "agents": { + "reviewer": { "disabled": false } + }, + "mcp": { + "servers": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "disabled": false + } + } + } +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-normalized.json b/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-normalized.json new file mode 100644 index 0000000000..b51b190f0f --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-normalized.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "modern": { + "enabled": false + } + }, + "snapshot": false, + "agent": { + "reviewer": { + "disable": true, + "options": {}, + "permission": {} + } + }, + "shell": "" +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-output.json b/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-output.json new file mode 100644 index 0000000000..38c069d5b5 --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-output.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://opencode.ai/config.json", + "snapshots": true, + "agents": { + "reviewer": { + "disabled": false + } + }, + "mcp": { + "servers": { + "modern": { + "type": "local", + "command": ["modern-mcp"], + "disabled": false + } + }, + "modern": { + "enabled": false + } + }, + "snapshot": false, + "agent": { + "reviewer": { + "disable": true, + "options": {}, + "permission": {} + } + }, + "shell": "" +} diff --git a/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-patch.json b/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-patch.json new file mode 100644 index 0000000000..5c25e7eb1c --- /dev/null +++ b/packages/opencode/test/config/fixtures/v2-compat/update-project/v1-overrides-patch.json @@ -0,0 +1,6 @@ +{ + "snapshot": false, + "agent": { "reviewer": { "disable": true } }, + "mcp": { "modern": { "enabled": false } }, + "shell": "" +} diff --git a/packages/opencode/test/config/snapshot.ts b/packages/opencode/test/config/snapshot.ts new file mode 100644 index 0000000000..d206570fdb --- /dev/null +++ b/packages/opencode/test/config/snapshot.ts @@ -0,0 +1,8 @@ +import { expect } from "bun:test" +import { ConfigParse } from "../../src/config/parse" + +export async function snapshot(file: string, actual: string) { + const value = ConfigParse.jsonc(actual, file) + if (process.env.UPDATE_CONFIG_FIXTURES === "1") await Bun.write(file, actual) + expect(value).toEqual(ConfigParse.jsonc(await Bun.file(file).text(), file)) +} diff --git a/packages/opencode/test/config/v2-compat.test.ts b/packages/opencode/test/config/v2-compat.test.ts new file mode 100644 index 0000000000..51873f0c4e --- /dev/null +++ b/packages/opencode/test/config/v2-compat.test.ts @@ -0,0 +1,400 @@ +import { describe, expect, test } from "bun:test" +import { ConfigV1 } from "@opencode-ai/core/v1/config/config" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { httpClient } from "@opencode-ai/core/effect/app-node-platform" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { Npm } from "@opencode-ai/core/npm" +import { Effect, Layer, Logger } from "effect" +import { HttpClient } from "effect/unstable/http" +import path from "path" +import { Account } from "../../src/account/account" +import { Auth } from "../../src/auth" +import { Config } from "../../src/config/config" +import { ConfigParse } from "../../src/config/parse" +import { ConfigV2Compat } from "../../src/config/v2-compat" +import { Env } from "../../src/env" +import { AccountTest } from "../fake/account" +import { AuthTest } from "../fake/auth" +import { NpmTest } from "../fake/npm" +import { TestInstance } from "../fixture/fixture" +import { testEffect } from "../lib/effect" +import { snapshot } from "./snapshot" + +const source = "test:v2-compat" +const lower = (input: unknown) => ConfigParse.schema(ConfigV1.Info, ConfigV2Compat.lower(input, source).value, source) + +const it = testEffect( + LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [ + [Auth.node, AuthTest.empty], + [Account.node, AccountTest.empty], + [Npm.node, NpmTest.noop], + [ + httpClient, + Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => Effect.die(`unexpected http request: ${request.method} ${request.url}`)), + ), + ], + ]), +) + +describe("V2 compatibility read fixtures", () => { + const directory = path.join(import.meta.dir, "fixtures/v2-compat/read") + const cases = Array.from(new Bun.Glob("*-input.jsonc").scanSync(directory)) + .map((file) => file.slice(0, -"-input.jsonc".length)) + .sort() + if (!cases.length) throw new Error("No V2 compatibility read fixtures found") + + cases.forEach((name) => { + test(name, async () => { + const source = path.join(directory, `${name}-input.jsonc`) + const input = ConfigParse.jsonc(await Bun.file(source).text(), source) + const original = structuredClone(input) + const result = ConfigV2Compat.lower(input, source) + expect(input).toEqual(original) + const config = ConfigParse.schema(ConfigV1.Info, result.value, source) + await snapshot(path.join(directory, `${name}-output.json`), JSON.stringify(config, null, 2) + "\n") + }) + }) +}) + +describe("ConfigV2Compat.lower", () => { + test("returns structured invalid diagnostics while retaining supported siblings", () => { + const result = ConfigV2Compat.lower({ + mcp: { + servers: { + broken: { type: "local", command: "not-an-array" }, + working: { type: "local", command: ["working-mcp"] }, + }, + }, + agents: { broken: { steps: "many" } }, + commands: { broken: { template: 42 } }, + }) + + expect(result.diagnostics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "invalid", path: ["mcp", "servers", "broken"] }), + expect.objectContaining({ kind: "invalid", path: ["agents", "broken"] }), + expect.objectContaining({ kind: "invalid", path: ["commands", "broken"] }), + ]), + ) + expect(ConfigParse.schema(ConfigV1.Info, result.value, source).mcp).toEqual({ + working: { type: "local", command: ["working-mcp"], enabled: true }, + }) + }) + + test("reports unsupported settings and lossy conversions without their values", () => { + const secret = "do-not-log-credentials" + const result = ConfigV2Compat.lower({ + model: { providerID: "example", model: "model", variant: "high" }, + plugins: [{ package: "native-plugin", options: { token: secret } }], + providers: { example: { settings: { apiKey: secret } } }, + websearch: secret, + warming: true, + experimental: { portable_shell_scanner: true }, + agents: { reviewer: { request: { headers: { Authorization: secret } } } }, + mcp: { + servers: { + remote: { + type: "remote", + url: `https://example.com/?token=${secret}`, + oauth: { client_secret: secret }, + codemode: false, + timeout: { execution: 60000 }, + }, + }, + }, + lsp: { custom: { command: ["custom-lsp"] } }, + }) + + expect(result.diagnostics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "unsupported", path: ["model", "variant"] }), + expect.objectContaining({ kind: "unsupported", path: ["plugins"] }), + expect.objectContaining({ kind: "unsupported", path: ["providers"] }), + expect.objectContaining({ kind: "unsupported", path: ["websearch"] }), + expect.objectContaining({ kind: "unsupported", path: ["warming"] }), + expect.objectContaining({ kind: "unsupported", path: ["experimental", "portable_shell_scanner"] }), + expect.objectContaining({ kind: "unsupported", path: ["agents", "reviewer", "request", "headers"] }), + expect.objectContaining({ kind: "unsupported", path: ["mcp", "servers", "remote", "codemode"] }), + expect.objectContaining({ kind: "unsupported", path: ["mcp", "servers", "remote", "timeout"] }), + expect.objectContaining({ kind: "unsupported", path: ["lsp", "custom"] }), + ]), + ) + expect(JSON.stringify(result.diagnostics)).not.toContain(secret) + }) + + test("reports conflicting forms while retaining the V1 value", () => { + const result = ConfigV2Compat.lower({ + snapshot: false, + snapshots: true, + command: { review: { template: "Legacy review" } }, + commands: { review: { template: "Native review" } }, + mcp: { + shared: { type: "local", command: ["legacy"] }, + servers: { shared: { type: "local", command: ["native"] } }, + }, + }) + const config = ConfigParse.schema(ConfigV1.Info, result.value, source) + + expect(config.snapshot).toBe(false) + expect(config.command?.review.template).toBe("Legacy review") + expect(config.mcp?.shared).toEqual({ type: "local", command: ["legacy"] }) + expect(result.diagnostics.filter((item) => item.kind === "conflict")).toHaveLength(3) + }) + + test("does not diagnose ordinary V1 configuration or reject invalid V1 roots early", () => { + expect(ConfigV2Compat.lower({ snapshot: false, mcp: { existing: { enabled: false } } }).diagnostics).toEqual([]) + expect(ConfigV2Compat.lower({ snapshot: false, snapshots: false }).diagnostics).toEqual([]) + const result = ConfigV2Compat.lower(null) + expect(result.value).toBeNull() + expect(() => ConfigParse.schema(ConfigV1.Info, result.value, source)).toThrow() + expect(() => lower({ snapshot: "invalid", snapshots: true })).toThrow() + }) + + test("keeps malformed MCP servers named servers and timeout for V1 validation", () => { + expect(() => lower({ mcp: { servers: { type: "local", command: "invalid" } } })).toThrow() + expect(() => lower({ mcp: { timeout: { type: "remote", url: 42 } } })).toThrow() + expect(() => lower({ mcp: { servers: { type: "bogus" } } })).toThrow() + expect(() => lower({ mcp: { servers: { type: 42 } } })).toThrow() + expect(() => lower({ mcp: { servers: { enabled: "false" } } })).toThrow() + }) + + test("rejects invalid V1 enablement when flat MCP entries include V2 fields", () => { + expect(() => + lower({ mcp: { invalid: { type: "local", command: ["legacy"], enabled: "false", codemode: false } } }), + ).toThrow("ConfigInvalidError") + }) + + test("does not repair malformed V1 containers or shadowed entries with V2 values", () => { + const cases = [ + { agent: null, agents: { reviewer: { system: "Native prompt" } } }, + { command: [], commands: { review: { template: "Native command" } } }, + { attachment: false, media: { image: { auto_resize: true } } }, + { experimental: null, mcp: { timeout: { catalog: 3000, execution: 3000 } } }, + { agent: { reviewer: 42 }, agents: { reviewer: { system: "Native prompt" } } }, + { command: { review: 42 }, commands: { review: { template: "Native command" } } }, + { mcp: { shared: 42, servers: { shared: { type: "local", command: ["native"] } } } }, + ] + cases.forEach((input) => expect(() => lower(input)).toThrow("ConfigInvalidError")) + }) + + test("keeps secrets out of invalid and conflict diagnostics", () => { + const secret = "secret-never-in-diagnostics" + const result = ConfigV2Compat.lower({ + commands: { malformed: { template: { token: secret } } }, + mcp: { + shared: { type: "remote", url: "https://example.com", headers: { Authorization: secret } }, + servers: { + shared: { type: "remote", url: "https://example.com", headers: { Authorization: `${secret}-changed` } }, + malformed: { type: "remote", url: `https://example.com?token=${secret}`, disabled: secret }, + }, + }, + }) + + expect(result.diagnostics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "conflict", path: ["mcp", "servers", "shared"] }), + expect.objectContaining({ kind: "invalid", path: ["commands", "malformed"] }), + expect.objectContaining({ kind: "invalid", path: ["mcp", "servers", "malformed"] }), + ]), + ) + expect(JSON.stringify(result.diagnostics)).not.toContain(secret) + }) + + test("rejects any native permission field, including empty and malformed values", () => { + const cases = [ + [{ action: "shell", resource: "*", effect: "deny" }], + [ + { action: "read", resource: "*", effect: "allow" }, + { action: "*", resource: "*", effect: "deny" }, + { action: "read", resource: "public", effect: "allow" }, + ], + [], + null, + "secret-permission-value", + [{ action: "read", resource: "secret-permission-value", effect: "invalid" }], + ] + cases.forEach((permissions) => { + expect(() => lower({ username: "keep-me", permissions })).toThrow("ConfigInvalidError") + expect(() => lower({ permission: "deny", permissions })).toThrow("ConfigInvalidError") + }) + }) + + test("does not repair invalid legacy permissions with native rules", () => { + expect(() => lower({ permission: { read: "invalid" }, permissions: [] })).toThrow("ConfigInvalidError") + }) + + test("rejects native agent permissions before decoding or applying V1 precedence", () => { + const cases = [ + { agents: { reviewer: { system: "Review carefully", permissions: [{ action: "read" }] } } }, + { agents: { reviewer: { disabled: true, permissions: [] } } }, + { agent: { reviewer: { permission: "deny" } }, agents: { reviewer: { permissions: [] } } }, + { agents: { reviewer: { steps: "invalid", permissions: [] } } }, + { agent: { reviewer: { permissions: [] } } }, + { mode: { reviewer: { permissions: [] } } }, + ] + cases.forEach((input) => expect(() => lower(input)).toThrow("ConfigInvalidError")) + }) + + test("continues to support V1 permission rules", () => { + const config = lower({ + permission: { bash: "deny", "*": "ask", edit: "allow" }, + agent: { reviewer: { permission: { edit: "deny" } } }, + }) + expect(config.permission).toEqual({ bash: "deny", "*": "ask", edit: "allow" }) + expect(Object.keys(config.permission ?? {})).toEqual(["bash", "*", "edit"]) + expect(config.agent?.reviewer?.permission).toEqual({ edit: "deny" }) + }) + + test("does not sanitize malformed V1 fields before schema validation", () => { + expect(() => lower({ model: 42 })).toThrow() + expect(() => lower({ snapshot: "enabled" })).toThrow() + expect(() => lower({ mcp: { broken: { type: "local", command: "not-an-array" } } })).toThrow() + expect(() => lower({ experimental: { mcp_timeout: -1 } })).toThrow() + }) + + test("does not mutate the input or nested configuration objects", () => { + const input = { + model: { providerID: "anthropic", model: "claude-sonnet", variant: "fast" }, + snapshots: true, + skills: ["./skills", "https://example.com/skills"], + mcp: { + existing: { type: "local", command: ["existing-mcp"], enabled: false }, + servers: { + native: { + type: "remote", + url: "https://example.com/mcp", + disabled: true, + oauth: { client_id: "client" }, + timeout: { execution: 3000 }, + }, + }, + }, + agents: { reviewer: { model: "anthropic/claude-sonnet#thinking", disabled: true } }, + experimental: { subagent_depth: 2 }, + } + const original = structuredClone(input) + + lower(input) + + expect(input).toEqual(original) + }) +}) + +describe("V2 configuration loading", () => { + it.instance("logs compatibility diagnostics without writing the lowered projection", () => + Effect.gen(function* () { + const instance = yield* TestInstance + const fs = yield* FSUtil.Service + const file = path.join(instance.directory, "opencode.jsonc") + const text = + '{\n // Retain this comment\n "$schema": "https://opencode.ai/config.json",\n "plugins": ["native-only"]\n}\n' + yield* fs.writeWithDirs(file, text) + const messages: unknown[] = [] + const config = yield* Config.use.get().pipe( + Effect.provide( + Logger.layer([ + Logger.make((options) => { + messages.push(options.message) + }), + ]), + ), + ) + + expect(config.plugin).toEqual([]) + expect(messages).toContainEqual([ + "configuration compatibility diagnostic", + expect.objectContaining({ source: file, kind: "unsupported", path: ["plugins"] }), + ]) + expect(yield* fs.readFileString(file)).toBe(text) + }), + ) + + it.instance("loads native V2 configuration through the V1 Config service", () => + Effect.gen(function* () { + const instance = yield* TestInstance + const fs = yield* FSUtil.Service + yield* fs.writeWithDirs( + path.join(instance.directory, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + model: { providerID: "anthropic", model: "claude-sonnet", variant: "fast" }, + snapshots: false, + skills: ["./skills", "https://example.com/skills"], + mcp: { + timeout: { catalog: 9000, execution: 9000 }, + servers: { + native: { type: "remote", url: "https://native.example.com/mcp", disabled: false }, + }, + }, + agents: { + reviewer: { + model: "anthropic/claude-sonnet#thinking", + system: "Review carefully.", + }, + }, + commands: { + review: { + template: "Review $ARGUMENTS", + model: { providerID: "anthropic", model: "claude-sonnet", variant: "thinking" }, + }, + }, + experimental: { subagent_depth: 2 }, + }), + ) + + const config = yield* Config.use.get() + + expect(config.model).toBe("anthropic/claude-sonnet") + expect(config.snapshot).toBe(false) + expect(config.skills).toEqual({ paths: ["./skills"], urls: ["https://example.com/skills"] }) + expect(config.mcp?.native).toEqual({ type: "remote", url: "https://native.example.com/mcp", enabled: true }) + expect(config.experimental?.mcp_timeout).toBe(9000) + expect(config.permission).toBeUndefined() + expect(config.agent?.reviewer).toMatchObject({ + model: "anthropic/claude-sonnet", + variant: "thinking", + prompt: "Review carefully.", + permission: {}, + }) + expect(config.command?.review).toMatchObject({ + template: "Review $ARGUMENTS", + model: "anthropic/claude-sonnet", + variant: "thinking", + }) + expect(config.subagent_depth).toBe(2) + }), + ) + + it.instance("keeps legacy TUI normalization when loading a mixed V1 and V2 document", () => + Effect.gen(function* () { + const instance = yield* TestInstance + const fs = yield* FSUtil.Service + yield* fs.writeWithDirs( + path.join(instance.directory, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + model: { providerID: "openai", model: "gpt-4.1" }, + theme: "legacy", + keybinds: { leader: "ctrl+x" }, + tui: { scroll_speed: 4 }, + mcp: { + legacy: { enabled: false }, + servers: { native: { type: "local", command: ["native-mcp"] } }, + }, + }), + ) + + const config = yield* Config.use.get() + + expect(config.model).toBe("openai/gpt-4.1") + expect(config.mcp?.legacy).toEqual({ enabled: false }) + expect(config.mcp?.native).toEqual({ type: "local", command: ["native-mcp"], enabled: true }) + expect(config).not.toHaveProperty("theme") + expect(config).not.toHaveProperty("keybinds") + expect(config).not.toHaveProperty("tui") + }), + ) +}) diff --git a/packages/opencode/test/control-plane/workspace.test.ts b/packages/opencode/test/control-plane/workspace.test.ts index a0d3aadbef..6d90eee2ae 100644 --- a/packages/opencode/test/control-plane/workspace.test.ts +++ b/packages/opencode/test/control-plane/workspace.test.ts @@ -8,6 +8,7 @@ import { Effect, Exit, Fiber, Layer, Schema } from "effect" import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { eq } from "drizzle-orm" import { GlobalBus, type GlobalEvent } from "@/bus/global" +import { Project } from "@/project/project" import { Database } from "@opencode-ai/core/database/database" import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" @@ -133,6 +134,9 @@ const startWorkspaceSyncingWithFlag = (projectID: ProjectV2.ID, experimentalWork Workspace.use.startWorkspaceSyncing(projectID).pipe(Effect.provide(workspaceLayer(experimentalWorkspaces))), ) +const listWithFlag = (project: Project.Info, experimentalWorkspaces: boolean) => + Effect.runPromise(Workspace.use.list(project).pipe(Effect.provide(workspaceLayer(experimentalWorkspaces)))) + function captureGlobalEvents() { const events: GlobalEvent[] = [] const handler = (event: GlobalEvent) => events.push(event) @@ -417,6 +421,18 @@ describe("workspace CRUD", () => { { git: true }, ) + it.instance( + "list is disabled by the experimental workspace flag", + () => + Effect.gen(function* () { + const instance = yield* requireInstance + yield* insertWorkspace(workspaceInfo(instance.project.id, "manual")) + + expect(yield* Effect.promise(() => listWithFlag(instance.project, false))).toEqual([]) + }), + { git: true }, + ) + it.instance( "create configures, persists, creates, starts local sync, and passes environment", () => diff --git a/packages/opencode/test/lib/cli-process.ts b/packages/opencode/test/lib/cli-process.ts index 12e8d9c866..a4d3b36c1d 100644 --- a/packages/opencode/test/lib/cli-process.ts +++ b/packages/opencode/test/lib/cli-process.ts @@ -211,7 +211,7 @@ export function withCliFixture( // on `Bun.stdin.text()` (see src/cli/cmd/run.ts — non-TTY stdin is // consumed as the prompt). The old Process.run wrapper defaulted to // ignore; ChildProcess.make defaults to pipe, so we set it explicitly. - const command = ChildProcess.make("bun", ["run", "--conditions=browser", cliEntry, ...args], { + const command = ChildProcess.make("bun", ["run", cliEntry, ...args], { cwd: home, env: { ...env, ...opts?.env }, extendEnv: true, @@ -283,7 +283,7 @@ export function withCliFixture( const options = runOpts(opts) const proc = yield* Effect.acquireRelease( Effect.sync(() => - Bun.spawn(["bun", "run", "--conditions=browser", cliEntry, ...runArgs(message, opts)], { + Bun.spawn(["bun", "run", cliEntry, ...runArgs(message, opts)], { cwd: home, env: { ...process.env, ...env, ...options?.env }, stdin: "ignore", @@ -324,7 +324,7 @@ export function withCliFixture( // as a finalizer error during test teardown. const proc = yield* Effect.acquireRelease( Effect.sync(() => - Bun.spawn(["bun", "run", "--conditions=browser", cliEntry, ...argv], { + Bun.spawn(["bun", "run", cliEntry, ...argv], { cwd: home, env: { ...process.env, ...env, ...opts?.env }, stdout: "pipe", @@ -395,7 +395,7 @@ export function withCliFixture( // Either way we await proc.exited so the test scope doesn't leak. const proc = yield* Effect.acquireRelease( Effect.sync(() => - Bun.spawn(["bun", "run", "--conditions=browser", cliEntry, ...argv], { + Bun.spawn(["bun", "run", cliEntry, ...argv], { cwd: opts?.cwd ?? home, env: { ...process.env, ...env, ...opts?.env }, stdin: "pipe", diff --git a/packages/opencode/test/patched-dependencies.test.ts b/packages/opencode/test/patched-dependencies.test.ts new file mode 100644 index 0000000000..5405401bbe --- /dev/null +++ b/packages/opencode/test/patched-dependencies.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test" +import path from "path" + +// Bun applies a patch only to the exact `name@version` named in +// `patchedDependencies`. Bumping the dependency without regenerating the patch +// does not fail `bun install`; the patch just stops applying and the runtime +// silently loses whatever the patch fixed. This pins the two together for the +// packages that ship in the CLI. +const root = path.resolve(import.meta.dir, "../../..") +const workspaces = ["packages/opencode", "packages/core"] +const patched = (await Bun.file(path.join(root, "package.json")).json()).patchedDependencies as Record + +describe("patched dependencies", () => { + for (const key of Object.keys(patched)) { + const at = key.lastIndexOf("@") + const name = key.slice(0, at) + const version = key.slice(at + 1) + + test(`${key} matches the installed version`, async () => { + expect(await Bun.file(path.join(root, patched[key])).exists()).toBe(true) + for (const workspace of workspaces) { + const file = Bun.file(path.join(root, workspace, "node_modules", name, "package.json")) + if (!(await file.exists())) continue + const installed = (await file.json()).version as string + expect(installed, `${workspace} resolves ${name}@${installed}; patch is for ${version}`).toBe(version) + } + }) + } +}) diff --git a/packages/opencode/test/plugin/azure.test.ts b/packages/opencode/test/plugin/azure.test.ts new file mode 100644 index 0000000000..2dcdfb0399 --- /dev/null +++ b/packages/opencode/test/plugin/azure.test.ts @@ -0,0 +1,280 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { chmod } from "node:fs/promises" +import path from "node:path" +import { pathToFileURL } from "node:url" +import { tmpdir } from "../fixture/fixture" +import type { Hooks } from "@opencode-ai/plugin" +import type { Auth, Provider } from "@opencode-ai/sdk/v2" +import { OAUTH_DUMMY_KEY } from "../../src/auth" +import { AzureAuthPlugin, createAzureAuthHooks } from "../../src/plugin/azure" +import { Process } from "../../src/util/process" +import { which } from "@opencode-ai/core/util/which" + +const resourceName = process.env.AZURE_RESOURCE_NAME +const originalPath = process.env.PATH + +afterEach(() => { + if (resourceName === undefined) delete process.env.AZURE_RESOURCE_NAME + else process.env.AZURE_RESOURCE_NAME = resourceName + if (originalPath === undefined) delete process.env.PATH + else process.env.PATH = originalPath +}) + +const oauth: Auth = { + type: "oauth", + access: OAUTH_DUMMY_KEY, + refresh: OAUTH_DUMMY_KEY, + expires: Date.now() + 60 * 60 * 1000, + accountId: "test-resource", +} + +const provider: Provider = { + id: "azure", + name: "Azure", + source: "custom", + env: [], + options: {}, + models: {}, +} + +function oauthMethod(hooks: Hooks) { + const method = hooks.auth?.methods.find((method) => method.type === "oauth") + if (!method || method.type !== "oauth") throw new Error("Azure OAuth method is missing") + return method +} + +function loader(hooks: Hooks) { + if (!hooks.auth?.loader) throw new Error("Azure auth loader is missing") + return hooks.auth.loader +} + +function customFetch(options: Record) { + const result = options["fetch"] + if (typeof result !== "function") throw new Error("Azure custom fetch is missing") + return async (input: RequestInfo | URL, init?: RequestInit) => { + const response: unknown = await Reflect.apply(result, undefined, [input, init]) + if (!(response instanceof Response)) throw new Error("Azure custom fetch returned an invalid response") + return response + } +} + +function azureShell(scopes: string[]) { + return async (args: string[]) => { + const scope = args[args.indexOf("--scope") + 1] + scopes.push(scope) + return { + accessToken: `${scope}-token`, + expires_on: Math.floor((Date.now() + 60 * 60 * 1000) / 1000), + } + } +} + +async function azureCli(dir: string) { + const bin = path.join(dir, "azure cli") + const calls = path.join(dir, "calls.jsonl") + const script = path.join(bin, "cli.cjs") + await Bun.write(calls, "") + await Bun.write( + script, + ` + const fs = require("node:fs") + const args = process.argv.slice(2) + fs.appendFileSync(${JSON.stringify(calls)}, JSON.stringify(args) + "\\n") + console.log(JSON.stringify(args.includes("get-access-token") + ? { accessToken: "test-token", expires_on: Math.floor(Date.now() / 1000) + 3600 } + : [])) + `, + ) + const executable = path.join(bin, process.platform === "win32" ? "az.cmd" : "az") + await Bun.write( + executable, + process.platform === "win32" + ? `@"${process.execPath}" "${script}" %*\r\n` + : `#!/bin/sh\nexec '${process.execPath}' '${script}' "$@"\n`, + ) + await chmod(executable, 0o755) + return { + bin, + calls: async () => + (await Bun.file(calls).text()) + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line)), + } +} + +describe("plugin.azure", () => { + test("initializes and runs Azure CLI under Node without Bun or a plugin shell", async () => { + await using tmp = await tmpdir() + const node = which("node") + if (!node) throw new Error("Node is required for the Azure runtime compatibility test") + const bundle = await Bun.build({ + entrypoints: [path.join(import.meta.dir, "../../src/plugin/azure.ts")], + target: "node", + format: "esm", + }) + expect(bundle.success).toBe(true) + const entry = path.join(tmp.path, "azure.mjs") + await Bun.write(entry, bundle.outputs[0]) + const cli = await azureCli(tmp.path) + for (const installed of [false, true]) { + const result = await Process.run( + [ + node, + "--input-type=module", + "-e", + ` + import assert from "node:assert/strict" + import { AzureAuthPlugin } from ${JSON.stringify(pathToFileURL(entry).href)} + assert.equal(typeof Bun, "undefined") + delete process.env.AZURE_RESOURCE_NAME + const hooks = await AzureAuthPlugin({ $: undefined }) + assert.equal(hooks.auth.provider, "azure") + assert.deepEqual(hooks.auth.methods.map((method) => method.type), ${JSON.stringify(installed ? ["api", "oauth"] : ["api"])}) + if (${installed}) { + const method = hooks.auth.methods.find((method) => method.type === "oauth") + assert.equal(method.prompts[0].type, "text") + const authorization = await method.authorize({ resourceName: "test-resource" }) + const auth = await authorization.callback() + assert.equal(auth.type, "success") + assert.equal(auth.accountId, "test-resource") + } + `, + ], + { + env: { PATH: installed ? cli.bin : tmp.path, XDG_DATA_HOME: tmp.path }, + nothrow: true, + }, + ) + expect(result.stderr.toString()).toBe("") + expect(result.code).toBe(0) + } + expect(await cli.calls()).toEqual([ + ["account", "get-access-token", "--scope", "https://cognitiveservices.azure.com/.default", "--output", "json"], + ]) + }) + + test("does not invoke Azure CLI during initialization", async () => { + await using tmp = await tmpdir() + const cli = await azureCli(tmp.path) + process.env.PATH = cli.bin + delete process.env.AZURE_RESOURCE_NAME + + const hooks = await AzureAuthPlugin() + + expect(await cli.calls()).toEqual([]) + expect(oauthMethod(hooks).prompts?.[0].type).toBe("text") + }) + + test("keeps the existing API-key method and adds Entra ID", () => { + delete process.env.AZURE_RESOURCE_NAME + const hooks = createAzureAuthHooks(azureShell([]), fetch, true) + + expect(hooks.auth?.provider).toBe("azure") + expect(hooks.auth?.methods.map((method) => [method.type, method.label])).toEqual([ + ["api", "API key"], + ["oauth", "Microsoft Entra ID (Azure CLI)"], + ]) + expect(hooks.auth?.methods[0]).toEqual({ + type: "api", + label: "API key", + prompts: [ + { + type: "text", + key: "resourceName", + message: "Enter Azure Resource Name", + placeholder: "e.g. my-models", + }, + ], + }) + expect(hooks.auth?.methods[1].prompts).toEqual(hooks.auth?.methods[0].prompts) + }) + + test("hides Azure CLI authentication when the Azure CLI is not installed", () => { + const hooks = createAzureAuthHooks(azureShell([]), fetch, false) + + expect(hooks.auth?.methods.map((method) => method.type)).toEqual(["api"]) + }) + + test("checks Azure CLI and stores the resource name", async () => { + const scopes: string[] = [] + const hooks = createAzureAuthHooks(azureShell(scopes), fetch, true) + const authorization = await oauthMethod(hooks).authorize({ resourceName: "test-resource" }) + if (authorization.method !== "auto") throw new Error("Unexpected Azure authorization method") + + expect(await authorization.callback()).toMatchObject({ + type: "success", + access: OAUTH_DUMMY_KEY, + refresh: OAUTH_DUMMY_KEY, + accountId: "test-resource", + }) + expect(scopes).toEqual(["https://cognitiveservices.azure.com/.default"]) + }) + + test("supports Azure CLI versions that only provide expiresOn", async () => { + const hooks = createAzureAuthHooks( + async () => ({ + accessToken: "legacy-token", + expiresOn: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + }), + fetch, + true, + ) + const authorization = await oauthMethod(hooks).authorize({ resourceName: "test-resource" }) + if (authorization.method !== "auto") throw new Error("Unexpected Azure authorization method") + + expect(await authorization.callback()).toMatchObject({ type: "success", accountId: "test-resource" }) + }) + + test("rejects Azure CLI tokens without a usable expiration", async () => { + const hooks = createAzureAuthHooks(async () => ({ accessToken: "invalid-token" }), fetch, true) + const authorization = await oauthMethod(hooks).authorize({ resourceName: "test-resource" }) + if (authorization.method !== "auto") throw new Error("Unexpected Azure authorization method") + + await expect(authorization.callback()).rejects.toThrow("Azure CLI returned an invalid token expiration") + }) + + test("does not change API-key loading", async () => { + const scopes: string[] = [] + const hooks = createAzureAuthHooks(azureShell(scopes), fetch, true) + + expect(await loader(hooks)(async () => ({ type: "api", key: "test-key" }), provider)).toEqual({}) + expect(scopes).toEqual([]) + }) + + test("uses Azure CLI bearer tokens for Azure inference endpoints", async () => { + const scopes: string[] = [] + const requests: Headers[] = [] + const hooks = createAzureAuthHooks( + azureShell(scopes), + async (_input, init) => { + requests.push(new Headers(init?.headers)) + return new Response(null, { status: 200 }) + }, + true, + ) + const options = await loader(hooks)(async () => oauth, provider) + const request = customFetch(options) + + await request("https://test-resource.openai.azure.com/openai/v1/responses", { + headers: { "api-key": OAUTH_DUMMY_KEY, "x-keep": "yes" }, + }) + await request("https://test-resource.services.ai.azure.com/models/chat/completions", { + headers: { Authorization: `Bearer ${OAUTH_DUMMY_KEY}` }, + }) + await request("https://test-resource.services.ai.azure.com/anthropic/v1/messages", { + headers: { "x-api-key": OAUTH_DUMMY_KEY }, + }) + + expect(scopes).toEqual(["https://cognitiveservices.azure.com/.default", "https://ai.azure.com/.default"]) + expect(requests.map((headers) => headers.get("authorization"))).toEqual([ + "Bearer https://cognitiveservices.azure.com/.default-token", + "Bearer https://cognitiveservices.azure.com/.default-token", + "Bearer https://ai.azure.com/.default-token", + ]) + expect(requests[0].get("api-key")).toBeNull() + expect(requests[0].get("x-keep")).toBe("yes") + expect(requests[2].get("x-api-key")).toBeNull() + expect(requests.every((headers) => headers.get("user-agent")?.startsWith("opencode/"))).toBe(true) + }) +}) diff --git a/packages/opencode/test/plugin/cerebras.test.ts b/packages/opencode/test/plugin/cerebras.test.ts new file mode 100644 index 0000000000..0a887e6d20 --- /dev/null +++ b/packages/opencode/test/plugin/cerebras.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "bun:test" +import type { Hooks, PluginInput } from "@opencode-ai/plugin" +import { CerebrasPlugin } from "../../src/plugin/cerebras" + +type ChatParams = NonNullable + +function input(npm: string) { + return { + model: { api: { npm } }, + } as Parameters[0] +} + +function output(options: Record) { + return { + maxOutputTokens: 32_000, + options, + } as Parameters[1] +} + +describe("CerebrasPlugin", () => { + test("omits the generic output cap when max_completion_tokens is configured", async () => { + const hook = (await CerebrasPlugin({} as PluginInput))["chat.params"]! + const params = output({ max_completion_tokens: 64 }) + + await hook(input("@ai-sdk/cerebras"), params) + + expect(params.maxOutputTokens).toBeUndefined() + }) + + test("preserves the generic output cap without max_completion_tokens", async () => { + const hook = (await CerebrasPlugin({} as PluginInput))["chat.params"]! + const params = output({}) + + await hook(input("@ai-sdk/cerebras"), params) + + expect(params.maxOutputTokens).toBe(32_000) + }) + + test("does not change other providers", async () => { + const hook = (await CerebrasPlugin({} as PluginInput))["chat.params"]! + const params = output({ max_completion_tokens: 64 }) + + await hook(input("@ai-sdk/openai"), params) + + expect(params.maxOutputTokens).toBe(32_000) + }) +}) diff --git a/packages/opencode/test/plugin/cloudflare.test.ts b/packages/opencode/test/plugin/cloudflare.test.ts index 5fa4106835..ab3d27df47 100644 --- a/packages/opencode/test/plugin/cloudflare.test.ts +++ b/packages/opencode/test/plugin/cloudflare.test.ts @@ -13,56 +13,13 @@ const pluginInput = { $: {} as never, } -function makeHookInput(overrides: { providerID?: string; apiId?: string; reasoning?: boolean }) { - return { - sessionID: "s", - agent: "a", - provider: {} as never, - message: {} as never, - model: { - providerID: overrides.providerID ?? "cloudflare-ai-gateway", - api: { id: overrides.apiId ?? "openai/gpt-5.2-codex", url: "", npm: "ai-gateway-provider" }, - capabilities: { - reasoning: overrides.reasoning ?? true, - temperature: false, - attachment: true, - toolcall: true, - input: { text: true, audio: false, image: false, video: false, pdf: false }, - output: { text: true, audio: false, image: false, video: false, pdf: false }, - interleaved: false, - }, - } as never, - } -} - -function makeHookOutput() { - return { temperature: 0, topP: 1, topK: 0, maxOutputTokens: 32_000 as number | undefined, options: {} } -} - -test("omits maxOutputTokens for openai reasoning models on cloudflare-ai-gateway", async () => { - const hooks = await CloudflareAIGatewayAuthPlugin(pluginInput) - const out = makeHookOutput() - await hooks["chat.params"]!(makeHookInput({ apiId: "openai/gpt-5.2-codex", reasoning: true }), out) - expect(out.maxOutputTokens).toBeUndefined() -}) - -test("keeps maxOutputTokens for openai non-reasoning models", async () => { - const hooks = await CloudflareAIGatewayAuthPlugin(pluginInput) - const out = makeHookOutput() - await hooks["chat.params"]!(makeHookInput({ apiId: "openai/gpt-4-turbo", reasoning: false }), out) - expect(out.maxOutputTokens).toBe(32_000) -}) - -test("keeps maxOutputTokens for non-openai reasoning models on cloudflare-ai-gateway", async () => { +test("registers the cloudflare-ai-gateway auth method", async () => { const hooks = await CloudflareAIGatewayAuthPlugin(pluginInput) - const out = makeHookOutput() - await hooks["chat.params"]!(makeHookInput({ apiId: "anthropic/claude-sonnet-4-5", reasoning: true }), out) - expect(out.maxOutputTokens).toBe(32_000) + expect(hooks.auth?.provider).toBe("cloudflare-ai-gateway") + expect(hooks.auth?.methods).toHaveLength(1) }) -test("ignores non-cloudflare-ai-gateway providers", async () => { +test("no longer drops maxOutputTokens; OpenAI models ride the Responses API passthrough", async () => { const hooks = await CloudflareAIGatewayAuthPlugin(pluginInput) - const out = makeHookOutput() - await hooks["chat.params"]!(makeHookInput({ providerID: "openai", apiId: "gpt-5.2-codex", reasoning: true }), out) - expect(out.maxOutputTokens).toBe(32_000) + expect(hooks["chat.params"]).toBeUndefined() }) diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts index 193858a07d..54ba034939 100644 --- a/packages/opencode/test/plugin/codex.test.ts +++ b/packages/opencode/test/plugin/codex.test.ts @@ -1,9 +1,13 @@ import { describe, expect, test } from "bun:test" +import { createServer, type IncomingMessage } from "node:http" +import { type AddressInfo } from "node:net" +import { WebSocketServer } from "ws" import { CodexAuthPlugin, parseJwtClaims, extractAccountIdFromClaims, extractAccountId, + extractResidency, renderOAuthError, type IdTokenClaims, } from "../../src/plugin/openai/codex" @@ -131,6 +135,69 @@ describe("plugin.codex", () => { }) }) + describe("extractResidency", () => { + test("extracts compute residency from the namespaced auth claims", () => { + expect( + extractResidency( + createTestJwt({ + "https://api.openai.com/auth": { chatgpt_compute_residency: "eu" }, + }), + ), + ).toBe("eu") + }) + + test("falls back to a root compute residency claim", () => { + expect(extractResidency(createTestJwt({ chatgpt_compute_residency: "us" }))).toBe("us") + }) + + test("supports compute residency values without maintaining a region list", () => { + expect( + extractResidency( + createTestJwt({ + "https://api.openai.com/auth": { chatgpt_compute_residency: "ae" }, + }), + ), + ).toBe("ae") + expect( + extractResidency( + createTestJwt({ + "https://api.openai.com/auth": { chatgpt_compute_residency: "future-region_1" }, + }), + ), + ).toBe("future-region_1") + }) + + test("ignores unconstrained and data residency values", () => { + expect( + extractResidency( + createTestJwt({ + "https://api.openai.com/auth": { chatgpt_compute_residency: "no_constraint" }, + }), + ), + ).toBeUndefined() + expect( + extractResidency( + createTestJwt({ + "https://api.openai.com/auth": { chatgpt_data_residency: "gb" }, + }), + ), + ).toBeUndefined() + expect(extractResidency(createTestJwt({ chatgpt_compute_residency: "" }))).toBeUndefined() + expect(extractResidency("not-a-jwt")).toBeUndefined() + }) + + test("prefers a namespaced unconstrained value over a root residency", () => { + expect( + extractResidency( + createTestJwt({ + chatgpt_compute_residency: "eu", + "https://api.openai.com/auth": { chatgpt_compute_residency: "no_constraint" }, + }), + ), + ).toBeUndefined() + }) + }) + test("installs websocket transport only when experimental websockets are enabled", async () => { const disabled = await CodexAuthPlugin({} as never) const enabled = await CodexAuthPlugin({} as never, { experimentalWebSockets: true }) @@ -149,6 +216,73 @@ describe("plugin.codex", () => { await enabled.dispose?.() }) + test("sends token residency only to the ChatGPT Codex backend", async () => { + const requests: Array<{ path: string; residency: string | null }> = [] + using server = Bun.serve({ + port: 0, + fetch(request) { + requests.push({ + path: new URL(request.url).pathname, + residency: request.headers.get("x-openai-internal-codex-residency"), + }) + return new Response("{}") + }, + }) + const hooks = await CodexAuthPlugin({} as never, { + codexApiEndpoint: new URL("/backend-api/codex/responses", server.url).toString(), + }) + const loaded = await hooks.auth!.loader!( + async () => + ({ + type: "oauth", + refresh: "refresh", + access: createTestJwt({ + "https://api.openai.com/auth": { chatgpt_compute_residency: "eu" }, + }), + expires: Date.now() + 60_000, + }) as never, + {} as never, + ) + + await loaded.fetch!("https://api.openai.com/v1/responses") + await loaded.fetch!(new URL("/other", server.url)) + + expect(requests).toEqual([ + { path: "/backend-api/codex/responses", residency: "eu" }, + { path: "/other", residency: null }, + ]) + }) + + test("sends token residency through the WebSocket transport", async () => { + await using server = await createCodexWebSocketServer() + const hooks = await CodexAuthPlugin({} as never, { + codexApiEndpoint: server.url, + experimentalWebSockets: true, + }) + const loaded = await hooks.auth!.loader!( + async () => + ({ + type: "oauth", + refresh: "refresh", + access: createTestJwt({ + "https://api.openai.com/auth": { chatgpt_compute_residency: "eu" }, + }), + expires: Date.now() + 60_000, + }) as never, + {} as never, + ) + + const response = await loaded.fetch!("https://api.openai.com/v1/responses", { + method: "POST", + headers: { "session-id": "session-1" }, + body: JSON.stringify({ stream: true, input: "hi" }), + }) + + expect(await response.text()).toContain("data: [DONE]") + expect(server.headers()?.["x-openai-internal-codex-residency"]).toBe("eu") + await hooks.dispose?.() + }) + test("filters unsupported modes and uses Codex context limits for OAuth GPT models", async () => { const hooks = await CodexAuthPlugin({} as never) const limit = { context: 1_050_000, input: 922_000, output: 128_000 } @@ -181,9 +315,9 @@ describe("plugin.codex", () => { expect(models["gpt-5.4"]?.limit).toEqual(limit) expect(models["gpt-5.5"]?.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 }) - expect(models["gpt-5.6-sol"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 }) - expect(models["gpt-5.6-terra"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 }) - expect(models["gpt-5.6-luna"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 }) + expect(models["gpt-5.6-sol"]?.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 }) + expect(models["gpt-5.6-terra"]?.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 }) + expect(models["gpt-5.6-luna"]?.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 }) expect(models["gpt-5.4-pro"]).toBeUndefined() expect(models["gpt-5.7-pro"]).toBeDefined() expect(models["gpt-5.6-sol-high"]).toBeDefined() @@ -192,7 +326,45 @@ describe("plugin.codex", () => { ) }) + test.each([ + ["gpt-6-astra", true], + ["gpt-6", true], + ["gpt-6.0-astra", true], + ["gpt-7", true], + ["gpt-10", true], + ["gpt-5.5-astra", true], + ["gpt-5.9", true], + ["gpt-5.10", true], + ["gpt-5.10-astra", true], + ["gpt-5.40", true], + ["gpt-5", false], + ["gpt-5.4-astra", false], + ["gpt-5.04-astra", false], + ["gpt-4.1", false], + ["gpt-4.99", false], + ["gpt-5.5-pro", false], + ["gpt-5.6", false], + ["gpt-6garbage", true], + ["gpt-6.", true], + ["gpt-6.1.2", true], + ["not-a-gpt-model", false], + ])("filters OAuth model %s by GPT major and minor versions", async (id, allowed) => { + const hooks = await CodexAuthPlugin({} as never) + const provider = { + models: { + [id]: { id, api: { id }, limit: {}, cost: {}, options: {} }, + }, + } + + const models = await hooks.provider!.models!(provider as never, { auth: { type: "oauth" } } as never) + + expect(Object.keys(models)).toEqual(allowed ? [id] : []) + }) + test("deduplicates concurrent Codex token refreshes", async () => { + const refreshedAccess = createTestJwt({ + "https://api.openai.com/auth": { chatgpt_compute_residency: "eu" }, + }) let auth = { type: "oauth" as const, refresh: "refresh-old", @@ -207,7 +379,7 @@ describe("plugin.codex", () => { resolveRefresh = resolve }) let refreshRequests = 0 - const apiRequests: { authorization: string | null; accountId: string | null }[] = [] + const apiRequests: { authorization: string | null; accountId: string | null; residency: string | null }[] = [] using server = Bun.serve({ port: 0, @@ -219,7 +391,7 @@ describe("plugin.codex", () => { await refreshReady return Response.json({ id_token: createTestJwt({ chatgpt_account_id: "acc-123" }), - access_token: "access-new", + access_token: refreshedAccess, refresh_token: "refresh-new", expires_in: 3600, }) @@ -229,6 +401,7 @@ describe("plugin.codex", () => { apiRequests.push({ authorization: request.headers.get("authorization"), accountId: request.headers.get("ChatGPT-Account-Id"), + residency: request.headers.get("x-openai-internal-codex-residency"), }) return new Response("{}", { status: 200 }) } @@ -286,11 +459,11 @@ describe("plugin.codex", () => { expect(refreshRequests).toBe(1) expect(authUpdates).toHaveLength(1) expect(authUpdates[0]?.body.refresh).toBe("refresh-new") - expect(authUpdates[0]?.body.access).toBe("access-new") + expect(authUpdates[0]?.body.access).toBe(refreshedAccess) expect(authUpdates[0]?.body.accountId).toBe("acc-123") expect(apiRequests).toEqual([ - { authorization: "Bearer access-new", accountId: "acc-123" }, - { authorization: "Bearer access-new", accountId: "acc-123" }, + { authorization: `Bearer ${refreshedAccess}`, accountId: "acc-123", residency: "eu" }, + { authorization: `Bearer ${refreshedAccess}`, accountId: "acc-123", residency: "eu" }, ]) }) }) @@ -302,3 +475,29 @@ async function waitFor(predicate: () => boolean) { await new Promise((resolve) => setTimeout(resolve, 1)) } } + +async function createCodexWebSocketServer() { + let headers: IncomingMessage["headers"] | undefined + const server = createServer() + const sockets = new WebSocketServer({ server }) + sockets.on("connection", (socket, request) => { + headers = request.headers + socket.once("message", () => { + socket.send(JSON.stringify({ type: "response.completed", response: { id: "resp_123" } })) + }) + }) + await new Promise((resolve, reject) => { + server.once("error", reject) + server.listen(0, "127.0.0.1", resolve) + }) + const address = server.address() as AddressInfo + return { + url: `http://127.0.0.1:${address.port}/backend-api/codex/responses`, + headers: () => headers, + async [Symbol.asyncDispose]() { + for (const socket of sockets.clients) socket.terminate() + sockets.close() + server.close() + }, + } +} diff --git a/packages/opencode/test/plugin/github-copilot.test.ts b/packages/opencode/test/plugin/github-copilot.test.ts new file mode 100644 index 0000000000..cd1864b795 --- /dev/null +++ b/packages/opencode/test/plugin/github-copilot.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from "bun:test" +import type { Hooks } from "@opencode-ai/plugin" +import { CopilotAuthPlugin } from "@/plugin/github-copilot/copilot" + +type ChatHeaders = NonNullable + +async function hook() { + const hooks = await CopilotAuthPlugin({ + directory: "", + project: {} as never, + worktree: "", + experimental_workspace: { register() {} }, + serverUrl: new URL("http://localhost"), + $: {} as never, + client: { + session: { + message: async () => ({ data: { parts: [] } }), + get: async () => ({ data: {} }), + }, + } as never, + }) + return hooks["chat.headers"]! +} + +function input(sessionID: string, providerID: string, npm: string) { + return { + sessionID, + agent: "build", + model: { providerID, api: { npm } }, + message: { id: "msg_test", sessionID }, + } as Parameters[0] +} + +test.each([ + ["github-copilot", "@ai-sdk/github-copilot"], + ["github-copilot", "@ai-sdk/anthropic"], + ["github-copilot-enterprise", "@ai-sdk/github-copilot"], + ["github-copilot-enterprise", "@ai-sdk/anthropic"], +])("uses the session ID for %s interaction headers with %s", async (providerID, npm) => { + const headers = await hook() + for (const sessionID of ["ses_one", "ses_one", "ses_two"]) { + const output = { headers: { "x-existing": "preserved" } } + await headers(input(sessionID, providerID, npm), output) + expect(output.headers).toMatchObject({ + "X-Interaction-Id": sessionID, + "x-existing": "preserved", + }) + } +}) + +test("does not add interaction headers to other providers", async () => { + const headers = await hook() + const output = { headers: { "x-existing": "preserved" } } + await headers(input("ses_one", "openai", "@ai-sdk/openai"), output) + expect(output.headers).toEqual({ "x-existing": "preserved" }) +}) diff --git a/packages/opencode/test/plugin/openai-ws.test.ts b/packages/opencode/test/plugin/openai-ws.test.ts index 04d20ca149..bec4be2c7b 100644 --- a/packages/opencode/test/plugin/openai-ws.test.ts +++ b/packages/opencode/test/plugin/openai-ws.test.ts @@ -17,13 +17,18 @@ describe("plugin.openai.ws", () => { const socket = await OpenAIWebSocket.connectResponsesWebSocket({ url: server.wsUrl, - headers: { authorization: "Bearer test", "content-length": "123" }, + headers: { + authorization: "Bearer test", + "content-length": "123", + "x-openai-internal-codex-residency": "eu", + }, }) expect(OpenAIWebSocket.toWebSocketUrl("http://example.com/v1/responses")).toBe("ws://example.com/v1/responses") expect(OpenAIWebSocket.toWebSocketUrl("https://example.com/v1/responses")).toBe("wss://example.com/v1/responses") expect(headers?.authorization).toBe("Bearer test") expect(headers?.["openai-beta"]).toBe(OpenAIWebSocket.PROTOCOL_HEADER) + expect(headers?.["x-openai-internal-codex-residency"]).toBe("eu") expect(headers?.["content-length"]).toBeUndefined() socket.terminate() }) @@ -141,7 +146,7 @@ describe("plugin.openai.ws", () => { }) }) -describe("plugin.openai.ws-pool", () => { +describe("plugin.openai.codex-responses-transport", () => { test("reuses one healthy websocket for sequential requests", async () => { let connections = 0 let messages = 0 @@ -230,6 +235,26 @@ describe("plugin.openai.ws-pool", () => { fetch.close() }) + test("falls back immediately to HTTP when a websocket request is too large", async () => { + let connections = 0 + await using server = await createWebSocketServer((socket) => { + connections += 1 + socket.once("message", () => socket.close(1009, "payload too large")) + }) + const fetch = CodexResponsesTransport.createWebSocketFetch({ + url: server.url, + }) + + const first = await fetch(server.url, streamRequest()) + const second = await fetch(server.url, streamRequest()) + + expect(await first.text()).toBe("http") + expect(await second.text()).toBe("http") + expect(connections).toBe(1) + expect(server.httpRequests).toHaveLength(2) + fetch.close() + }) + test("removes HTTP fallback when its session is deleted", async () => { let websocketAttempts = 0 await using server = await createRejectingWebSocketServer(() => websocketAttempts++) diff --git a/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts b/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts index f062868c42..97f9d4f909 100644 --- a/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts +++ b/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts @@ -1,22 +1,26 @@ -// End-to-end regression test for opencode#24432. +// End-to-end regression tests for opencode#24432 and opencode#32051/#32052. // -// Routes through the actual ai-gateway-provider + @ai-sdk/openai-compatible -// chain that provider.ts:811 builds at runtime, with only the network boundary -// stubbed. Asserts that `reasoning_effort` (and other provider options the -// transform emits) actually land in the body Cloudflare AI Gateway forwards -// upstream, which is the only place the bug was observable. +// Routes through the actual ai-gateway-provider chain that provider.ts builds at +// runtime, with only the network boundary stubbed: +// - openai/* -> native OpenAI passthrough (Responses API) +// - anthropic/* -> native Anthropic passthrough (Messages API) +// - everything else -> unified /compat (openai-compatible chat completions) +// Asserts what actually lands in the envelope body Cloudflare AI Gateway +// forwards upstream, which is the only place these bugs were observable. import { afterEach, beforeEach, describe, expect, test } from "bun:test" import type { JSONValue } from "ai" import { generateText } from "ai" import { createAiGateway } from "ai-gateway-provider" import { createUnified } from "ai-gateway-provider/providers/unified" +import { createOpenAI } from "ai-gateway-provider/providers/openai" +import { createAnthropic } from "ai-gateway-provider/providers/anthropic" import { ProviderTransform } from "@/provider/transform" import type * as Provider from "@/provider/provider" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" -type Captured = { url: string; outerBody: unknown } +type Captured = { url: string; outerBody: unknown; headers: Record } type ProviderOptions = Record> const realFetch = globalThis.fetch @@ -26,24 +30,76 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value) } +// The gateway returns the upstream provider's response body verbatim, so the +// mock must answer in the wire format of the step's target provider. +function upstreamResponseBody(provider: string | undefined) { + if (provider === "openai") + return { + id: "resp_test", + object: "response", + created_at: 0, + model: "gpt-5.4", + status: "completed", + error: null, + incomplete_details: null, + output: [ + { + type: "message", + role: "assistant", + id: "msg_1", + status: "completed", + content: [{ type: "output_text", text: "ok", annotations: [] }], + }, + ], + usage: { + input_tokens: 1, + input_tokens_details: { cached_tokens: 0 }, + output_tokens: 1, + output_tokens_details: { reasoning_tokens: 0 }, + total_tokens: 2, + }, + } + if (provider === "anthropic") + return { + id: "msg_test", + type: "message", + role: "assistant", + model: "claude-sonnet-4-6", + content: [{ type: "text", text: "ok" }], + stop_reason: "end_turn", + stop_sequence: null, + usage: { input_tokens: 1, output_tokens: 1 }, + } + return { + id: "chatcmpl-test", + object: "chat.completion", + created: 0, + model: "test", + choices: [{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } +} + beforeEach(() => { captured = null const handle = async (input: Parameters[0], init?: Parameters[1]): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url if (url.startsWith("https://gateway.ai.cloudflare.com/")) { const bodyText = typeof init?.body === "string" ? init.body : "" - captured = { url, outerBody: bodyText ? JSON.parse(bodyText) : null } - return new Response( - JSON.stringify({ - id: "chatcmpl-test", - object: "chat.completion", - created: 0, - model: "openai/gpt-5.4", - choices: [{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" }], - usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ) + const outerBody = bodyText ? JSON.parse(bodyText) : null + captured = { + url, + outerBody, + headers: Object.fromEntries(new Headers(init?.headers).entries()), + } + const provider = + Array.isArray(outerBody) && isRecord(outerBody[0]) && typeof outerBody[0].provider === "string" + ? outerBody[0].provider + : undefined + return new Response(JSON.stringify(upstreamResponseBody(provider)), { + status: 200, + headers: { "Content-Type": "application/json" }, + }) } return realFetch(input, init) } @@ -56,11 +112,19 @@ afterEach(() => { globalThis.fetch = realFetch }) +// Mirrors the runtime npm rewrite in provider.ts: openai/anthropic models carry +// their native SDK package so transforms key provider options correctly. +const cfNpm = (apiId: string) => { + if (apiId.startsWith("openai/")) return "@ai-sdk/openai" + if (apiId.startsWith("anthropic/")) return "@ai-sdk/anthropic" + return "ai-gateway-provider" +} + const cfModel = (apiId: string, releaseDate = "2026-03-05"): Provider.Model => ({ id: ModelV2.ID.make(`cloudflare-ai-gateway/${apiId}`), providerID: ProviderV2.ID.make("cloudflare-ai-gateway"), name: apiId, - api: { id: apiId, url: "https://gateway.ai.cloudflare.com/v1/compat", npm: "ai-gateway-provider" }, + api: { id: apiId, url: "https://gateway.ai.cloudflare.com/v1/compat", npm: cfNpm(apiId) }, capabilities: { reasoning: true, temperature: false, @@ -80,53 +144,177 @@ const cfModel = (apiId: string, releaseDate = "2026-03-05"): Provider.Model => ( // ai-gateway-provider sends an array of step descriptors; each entry's `query` // is the body forwarded to the upstream provider. -function extractUpstreamQuery(body: unknown): Record | undefined { +function firstStep(body: unknown): Record | undefined { if (!Array.isArray(body) || body.length === 0) return undefined const first = body[0] - if (!isRecord(first)) return undefined - const query = first.query + return isRecord(first) ? first : undefined +} + +function extractUpstreamQuery(body: unknown): Record | undefined { + const query = firstStep(body)?.query return isRecord(query) ? query : undefined } -async function callThroughGateway(apiId: string, providerOptions: ProviderOptions) { - const aigateway = createAiGateway({ accountId: "test", gateway: "test", apiKey: "test" }) - const unified = createUnified() - await generateText({ model: aigateway(unified(apiId)), prompt: "hi", providerOptions }) +// Each step descriptor also carries the `headers` forwarded to the upstream provider. +function extractUpstreamHeaders(body: unknown): Record | undefined { + const headers = firstStep(body)?.headers + return isRecord(headers) ? headers : undefined +} + +// Mirrors the runtime routing in provider.ts getModel. +function gatewayModel(apiId: string, gatewayToken = "test") { + const aigateway = createAiGateway({ accountId: "test", gateway: "test", apiKey: gatewayToken }) + if (apiId.startsWith("openai/")) return aigateway(createOpenAI()(apiId.slice("openai/".length))) + if (apiId.startsWith("anthropic/")) + return aigateway(createAnthropic()(apiId.slice("anthropic/".length).replaceAll(".", "-"))) + const isWorkersAi = apiId.startsWith("workers-ai/") || apiId.startsWith("@cf/") + const unified = createUnified(isWorkersAi ? { apiKey: gatewayToken } : {}) + return aigateway(unified(apiId)) +} + +async function callThroughGateway(apiId: string, providerOptions: ProviderOptions, gatewayToken = "test") { + await generateText({ model: gatewayModel(apiId, gatewayToken), prompt: "hi", providerOptions }) return extractUpstreamQuery(captured?.outerBody) } +describe("cf-ai-gateway routing", () => { + test("openai/* rides the native OpenAI passthrough on the Responses API", async () => { + await callThroughGateway("openai/gpt-5.4", {}) + const step = firstStep(captured?.outerBody) + expect(step?.provider).toBe("openai") + expect(step?.endpoint).toBe("v1/responses") + const upstream = extractUpstreamQuery(captured?.outerBody) + expect(upstream?.model).toBe("gpt-5.4") + }) + + test("anthropic/* rides the native Anthropic passthrough on the Messages API", async () => { + await callThroughGateway("anthropic/claude-sonnet-4-6", {}) + const step = firstStep(captured?.outerBody) + expect(step?.provider).toBe("anthropic") + expect(step?.endpoint).toBe("v1/messages") + const upstream = extractUpstreamQuery(captured?.outerBody) + expect(upstream?.model).toBe("claude-sonnet-4-6") + }) + + test("anthropic/* with a dotted models.dev id reaches Anthropic as a dashed native slug", async () => { + // models.dev ids are dotted (claude-haiku-4.5); Anthropic's Messages API 404s unless the + // version is dashed (claude-haiku-4-5). Regression guard for the dotted-id translation. + await callThroughGateway("anthropic/claude-haiku-4.5", {}) + const step = firstStep(captured?.outerBody) + expect(step?.provider).toBe("anthropic") + expect(step?.endpoint).toBe("v1/messages") + const upstream = extractUpstreamQuery(captured?.outerBody) + expect(upstream?.model).toBe("claude-haiku-4-5") + }) + + test("workers-ai models stay on the unified /compat route", async () => { + await callThroughGateway("workers-ai/@cf/moonshotai/kimi-k2.6", {}) + const step = firstStep(captured?.outerBody) + expect(step?.provider).toBe("compat") + expect(step?.endpoint).toBe("chat/completions") + const upstream = extractUpstreamQuery(captured?.outerBody) + expect(upstream?.model).toBe("workers-ai/@cf/moonshotai/kimi-k2.6") + }) +}) + describe("cf-ai-gateway end-to-end (regression: #24432)", () => { - test("ProviderTransform.providerOptions output puts reasoning_effort on the wire", async () => { - // The full chain the runtime exercises: - // transform.providerOptions() -> openaiCompatible key - // -> @ai-sdk/openai-compatible reads it as compatibleOptions - // -> emits body.reasoning_effort + test("ProviderTransform.providerOptions output puts reasoning effort on the Responses wire", async () => { + // The full chain the runtime exercises for OpenAI models: + // transform.providerOptions() -> "openai" key (npm rewritten to @ai-sdk/openai) + // -> OpenAIResponsesLanguageModel emits body.reasoning.effort // -> ai-gateway-provider wraps the body and forwards to gateway.ai.cloudflare.com const opts = ProviderTransform.providerOptions(cfModel("openai/gpt-5.4"), { reasoningEffort: "xhigh" }) - expect(opts).toEqual({ openaiCompatible: { reasoningEffort: "xhigh" } }) + expect(Object.keys(opts)).toEqual(["openai"]) + expect(opts.openai.reasoningEffort).toBe("xhigh") const upstream = await callThroughGateway("openai/gpt-5.4", opts) - expect(upstream?.reasoning_effort).toBe("xhigh") + expect((upstream?.reasoning as Record | undefined)?.effort).toBe("xhigh") }) test("variants() output for openai/gpt-5.4 lands xhigh on the wire", async () => { - // The other half of the bug: workflow `variant: xhigh` flows through variants() - // and must reach the wire. variants() returns the providerOptions payload - // unwrapped; providerOptions() wraps it under the SDK key. + // fromModelsDevModel resolves the native npm before computing variants, so + // OpenAI models get full Responses-flavored payloads (summary + encrypted + // reasoning include for stateless multi-turn reasoning). const variants = ProviderTransform.variants(cfModel("openai/gpt-5.4")) - expect(variants.xhigh).toEqual({ reasoningEffort: "xhigh" }) + expect(variants.xhigh).toEqual({ + reasoningEffort: "xhigh", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + }) const opts = ProviderTransform.providerOptions(cfModel("openai/gpt-5.4"), variants.xhigh) const upstream = await callThroughGateway("openai/gpt-5.4", opts) - expect(upstream?.reasoning_effort).toBe("xhigh") + const reasoning = upstream?.reasoning as Record | undefined + expect(reasoning?.effort).toBe("xhigh") + expect(reasoning?.summary).toBe("auto") + }) + + test("reasoning effort variants for anthropic models land as native adaptive thinking", async () => { + // Mirrors the runtime catalog path: models.dev reasoning_options -> reasoningVariants + // computed on the native @ai-sdk/anthropic npm -> adaptive thinking + output_config.effort. + const model = cfModel("anthropic/claude-sonnet-4-6") + const variants = ProviderTransform.reasoningVariants( + { reasoning_options: [{ type: "effort", values: ["low", "medium", "high"] }] } as never, + model, + ) + expect(variants?.high).toMatchObject({ effort: "high" }) + + const opts = ProviderTransform.providerOptions(model, variants!.high) + expect(Object.keys(opts)).toEqual(["anthropic"]) + + const upstream = await callThroughGateway("anthropic/claude-sonnet-4-6", opts) + expect((upstream?.thinking as Record | undefined)?.type).toBe("adaptive") + expect((upstream?.output_config as Record | undefined)?.effort).toBe("high") + }) + + test("reasoning_effort still reaches the /compat wire for workers-ai models", async () => { + const model = cfModel("workers-ai/@cf/moonshotai/kimi-k2.6") + const opts = ProviderTransform.providerOptions(model, { reasoningEffort: "high" }) + expect(opts).toEqual({ openaiCompatible: { reasoningEffort: "high" } }) + + const upstream = await callThroughGateway("workers-ai/@cf/moonshotai/kimi-k2.6", opts) + expect(upstream?.reasoning_effort).toBe("high") }) test("legacy buggy key 'cloudflare-ai-gateway' does NOT reach the wire (proves the bug)", async () => { // Sanity: confirms the bug class. If a future change accidentally restores // providerID-keyed providerOptions, this test fails before users notice. - const upstream = await callThroughGateway("openai/gpt-5.4", { + const upstream = await callThroughGateway("workers-ai/@cf/moonshotai/kimi-k2.6", { "cloudflare-ai-gateway": { reasoningEffort: "high" }, }) expect(upstream?.reasoning_effort).toBeUndefined() }) }) + +describe("cf-ai-gateway token scoping (regression: #32051/#32052)", () => { + test("openai passthrough does NOT forward the Cloudflare token upstream", async () => { + await callThroughGateway("openai/gpt-5.4", {}, "cf-gateway-secret") + + expect(captured?.headers["cf-aig-authorization"]).toBe("Bearer cf-gateway-secret") + // Security invariant: the Cloudflare token must never become the upstream provider's Authorization. + expect(extractUpstreamHeaders(captured?.outerBody)?.["authorization"]).toBeUndefined() + expect(JSON.stringify(captured?.outerBody)).not.toContain("cf-gateway-secret") + }) + + test("anthropic passthrough does NOT forward the Cloudflare token upstream", async () => { + await callThroughGateway("anthropic/claude-sonnet-4-6", {}, "cf-gateway-secret") + + expect(captured?.headers["cf-aig-authorization"]).toBe("Bearer cf-gateway-secret") + expect(extractUpstreamHeaders(captured?.outerBody)?.["x-api-key"]).toBeUndefined() + expect(JSON.stringify(captured?.outerBody)).not.toContain("cf-gateway-secret") + }) + + test("workers-ai models DO forward the Cloudflare token upstream", async () => { + await callThroughGateway("workers-ai/@cf/google/gemma-4-26b-a4b-it", {}, "cf-gateway-secret") + + expect(captured?.headers["cf-aig-authorization"]).toBe("Bearer cf-gateway-secret") + expect(extractUpstreamHeaders(captured?.outerBody)?.["authorization"]).toBe("Bearer cf-gateway-secret") + }) + + test("bare @cf/ Workers AI models DO forward the Cloudflare token upstream", async () => { + await callThroughGateway("@cf/meta/llama-3.1-8b-instruct", {}, "cf-gateway-secret") + + expect(captured?.headers["cf-aig-authorization"]).toBe("Bearer cf-gateway-secret") + expect(extractUpstreamHeaders(captured?.outerBody)?.["authorization"]).toBe("Bearer cf-gateway-secret") + }) +}) diff --git a/packages/opencode/test/provider/error.test.ts b/packages/opencode/test/provider/error.test.ts new file mode 100644 index 0000000000..49db280662 --- /dev/null +++ b/packages/opencode/test/provider/error.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test" +import { ProviderError } from "@/provider/error" + +describe("provider stream errors", () => { + test("retries provider stream errors without a code", () => { + const messages = [ + "The model is currently at capacity due to high demand. Please try again in a few minutes, or use a higher service tier for priority processing: https://docs.x.ai/developers/advanced-api-usage/priority-processing", + "The model is temporarily unavailable.", + ] + + for (const message of messages) + expect( + ProviderError.parseStreamError({ + type: "error", + error: { message }, + }), + ).toEqual({ + type: "api_error", + message, + isRetryable: true, + responseBody: JSON.stringify({ type: "error", error: { message } }), + }) + }) +}) diff --git a/packages/opencode/test/provider/header-timeout.test.ts b/packages/opencode/test/provider/header-timeout.test.ts index fc5ab04e10..38b884dc7f 100644 --- a/packages/opencode/test/provider/header-timeout.test.ts +++ b/packages/opencode/test/provider/header-timeout.test.ts @@ -13,6 +13,8 @@ import { Env } from "@/env" import { Plugin } from "@/plugin" import { Provider } from "@/provider/provider" import { ProviderError } from "@/provider/error" +import { MessageV2 } from "@/session/message-v2" +import { SessionRetry } from "@/session/retry" afterEach(async () => { await disposeAllInstances() @@ -46,7 +48,48 @@ it.live("headerTimeout does not abort delayed SSE body after headers arrive", () }), ) -it.live("chunkTimeout raises a response stream error when SSE body stalls", () => +for (const timeout of ["chunkTimeout", "headerTimeout"] as const) { + it.live(`default ${timeout} is applied at fetch without changing provider options`, () => + Effect.gen(function* () { + const server = yield* Effect.acquireRelease( + Effect.promise(() => delayedBodyServer(250)), + (server) => Effect.sync(() => server.server.close()), + ) + + yield* provideTmpdirInstance( + () => + Effect.gen(function* () { + const provider = yield* Provider.Service + const configured = yield* provider.getProvider(ProviderV2.ID.make("test")) + const signals: (AbortSignal | null | undefined)[] = [] + configured.options.fetch = (input: RequestInfo | URL, init?: RequestInit) => { + signals.push(init?.signal) + return fetch(input, init) + } + const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model")) + const language = yield* provider.getLanguage(model) + yield* Effect.acquireRelease( + Effect.promise(() => + language.doStream({ prompt: [{ role: "user", content: [{ type: "text", text: "hello" }] }] }), + ), + (result) => Effect.promise(() => result.stream.cancel()), + ) + + expect(signals).toHaveLength(1) + expect(signals[0]).toBeInstanceOf(AbortSignal) + expect(configured.options[timeout]).toBeUndefined() + }), + { + config: providerConfig(server.url, { + [timeout === "chunkTimeout" ? "headerTimeout" : "chunkTimeout"]: false, + }), + }, + ) + }), + ) +} + +it.live("configured chunkTimeout raises a retryable response stream error when SSE body stalls", () => Effect.gen(function* () { const server = yield* Effect.acquireRelease( Effect.promise(() => delayedBodyServer(250)), @@ -74,12 +117,41 @@ it.live("chunkTimeout raises a response stream error when SSE body stalls", () = } }) expect(error).toBeInstanceOf(ProviderError.ResponseStreamError) + expect( + SessionRetry.retryable(MessageV2.fromError(error, { providerID: model.providerID }), model.providerID), + ).toEqual({ message: "SSE read timed out" }) }), { config: providerConfig(server.url, { chunkTimeout: 50 }) }, ) }), ) +it.live("chunkTimeout can be disabled with false", () => + Effect.gen(function* () { + const server = yield* Effect.acquireRelease( + Effect.promise(() => delayedBodyServer(250)), + (server) => Effect.sync(() => server.server.close()), + ) + + yield* provideTmpdirInstance( + () => + Effect.gen(function* () { + const provider = yield* Provider.Service + const configured = yield* provider.getProvider(ProviderV2.ID.make("test")) + expect(configured.options.chunkTimeout).toBe(false) + const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model")) + const result = streamText({ + model: yield* provider.getLanguage(model), + messages: [{ role: "user", content: "hello" }], + }) + + expect(yield* Effect.promise(() => result.text)).toBe("late") + }), + { config: providerConfig(server.url, { chunkTimeout: false }) }, + ) + }), +) + it.live("headerTimeout aborts when response headers do not arrive", () => Effect.gen(function* () { const server = yield* Effect.acquireRelease( @@ -112,7 +184,7 @@ it.live("headerTimeout aborts when response headers do not arrive", () => }), ) -it.live("headerTimeout is opt-in for non-OpenAI providers", () => +it.live("headerTimeout can be disabled with false for non-OpenAI providers", () => Effect.gen(function* () { const server = yield* Effect.acquireRelease( Effect.promise(() => delayedHeaderServer(100)), @@ -131,12 +203,12 @@ it.live("headerTimeout is opt-in for non-OpenAI providers", () => expect(yield* Effect.promise(() => result.text)).toBe("ok") }), - { config: providerConfig(server.url) }, + { config: providerConfig(server.url, { headerTimeout: false }) }, ) }), ) -it.live("OpenAI Codex headerTimeout default can be disabled by config", () => +it.live("OpenAI Codex header and chunk timeout defaults can be disabled by config", () => Effect.gen(function* () { yield* withAuthContent( Effect.gen(function* () { @@ -146,8 +218,9 @@ it.live("OpenAI Codex headerTimeout default can be disabled by config", () => const provider = yield* Provider.Service const openai = yield* provider.getProvider(ProviderV2.ID.openai) expect(openai.options.headerTimeout).toBe(false) + expect(openai.options.chunkTimeout).toBe(false) }), - { config: { provider: { openai: { options: { headerTimeout: false } } } } }, + { config: { provider: { openai: { options: { headerTimeout: false, chunkTimeout: false } } } } }, ) }), ) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index df23a5c496..32d4e3a39b 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1909,6 +1909,32 @@ it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () => }), ) +it.instance("Google Vertex: uses REP endpoint for Gemini continental multi-regions", () => + Effect.gen(function* () { + yield* set("GOOGLE_CLOUD_PROJECT", "test-project") + yield* set("VERTEX_LOCATION", "eu") + const provider = yield* Provider.Service + const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini-3.5-flash")) + const language = yield* provider.getLanguage(model) + expect(languageBaseURL(language)).toBe( + "https://aiplatform.eu.rep.googleapis.com/v1beta1/projects/test-project/locations/eu/publishers/google", + ) + }), +) + +it.instance("Google Vertex: keeps regional Gemini endpoints unchanged", () => + Effect.gen(function* () { + yield* set("GOOGLE_CLOUD_PROJECT", "test-project") + yield* set("VERTEX_LOCATION", "europe-west1") + const provider = yield* Provider.Service + const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini-3.5-flash")) + const language = yield* provider.getLanguage(model) + expect(languageBaseURL(language)).toBe( + "https://europe-west1-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/europe-west1/publishers/google", + ) + }), +) + it.instance("cloudflare-ai-gateway loads with env variables", () => Effect.gen(function* () { yield* set("CLOUDFLARE_ACCOUNT_ID", "test-account") diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 918fa056c4..89028c83d8 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -5,7 +5,10 @@ import { LLMRequestPrep } from "@/session/llm/request" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { ModelsDev } from "@opencode-ai/core/models-dev" -import { jsonSchema } from "ai" +import { generateText, jsonSchema, type ModelMessage } from "ai" +import { createAmazonBedrock, type AmazonBedrockLanguageModelOptions } from "@ai-sdk/amazon-bedrock" +import { createAnthropic } from "@ai-sdk/anthropic" +import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic" describe("ProviderTransform.options - setCacheKey", () => { const sessionID = "test-session-123" @@ -523,6 +526,7 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => { expect(result.reasoningEffort).toBe("medium") expect(result.reasoningSummary).toBeUndefined() expect(result.include).toBeUndefined() + expect(result.textVerbosity).toBeUndefined() }) test("azure chat completions omit Responses-only reasoning options after variants merge", async () => { @@ -844,6 +848,354 @@ describe("ProviderTransform.providerOptions", () => { }) }) + describe("anthropic thinking block binding", () => { + const binding = { prefixMismatchBehavior: "drop_block" } + const claude = (npm: string, id: string) => + createModel({ providerID: "custom", api: { id, url: "https://example.com", npm } }) + const sdks = [ + { npm: "@ai-sdk/anthropic", key: "anthropic", option: "thinking" }, + { npm: "@ai-sdk/google-vertex/anthropic", key: "anthropic", option: "thinking" }, + { npm: "@ai-sdk/amazon-bedrock", key: "bedrock", option: "reasoningConfig" }, + ] + + test("adds blockBinding to explicit adaptive thinking on @ai-sdk/anthropic", () => { + const model = claude("@ai-sdk/anthropic", "claude-fable-5-1") + expect(ProviderTransform.providerOptions(model, { thinking: { type: "adaptive" }, effort: "high" })).toEqual({ + anthropic: { thinking: { type: "adaptive", blockBinding: binding }, effort: "high" }, + }) + }) + + test("leaves explicit enabled thinking on older models alone", () => { + const model = claude("@ai-sdk/anthropic", "claude-haiku-4-5") + expect(ProviderTransform.providerOptions(model, { thinking: { type: "enabled", budgetTokens: 4000 } })).toEqual({ + anthropic: { thinking: { type: "enabled", budgetTokens: 4000 } }, + }) + }) + + sdks.forEach((sdk) => { + describe(sdk.npm, () => { + test.each([ + "claude-fable-5-1", + "claude-fable-5.1", + "claude-5.1-fable", + "global.anthropic.claude-fable-5-1", + "us.anthropic.claude-fable-5-1-v1:0", + "claude-fable-5-1@default", + "CLAUDE-FABLE-5-1", + "claude-opus-5-1", + "claude-sonnet-5-2", + "claude-mythos-5-2", + "claude-mythos-5-10", + "claude-opus-6", + "claude-6-opus", + "claude-mythos-6-20270901", + ])("adds binding for %s", (id) => { + const model = claude(sdk.npm, id) + expect(ProviderTransform.providerOptions(model, {})).toEqual({ + [sdk.key]: { [sdk.option]: { type: "adaptive", blockBinding: binding } }, + }) + expect( + ProviderTransform.providerOptions(model, { [sdk.option]: { type: "adaptive", display: "summarized" } }), + ).toEqual({ + [sdk.key]: { [sdk.option]: { type: "adaptive", display: "summarized", blockBinding: binding } }, + }) + expect( + ProviderTransform.providerOptions(model, { [sdk.option]: { type: "enabled", budgetTokens: 4000 } }), + ).toEqual({ + [sdk.key]: { [sdk.option]: { type: "enabled", budgetTokens: 4000, blockBinding: binding } }, + }) + expect(ProviderTransform.providerOptions(model, { [sdk.option]: { type: "disabled" } })).toEqual({ + [sdk.key]: { [sdk.option]: { type: "disabled" } }, + }) + }) + + test.each([ + "claude-haiku-4-5", + "claude-opus-4-8", + "claude-sonnet-4-6", + "claude-opus-5", + "claude-sonnet-5", + "claude-fable-5", + "claude-opus-5-0", + "claude-opus-5-20260724", + "global.anthropic.claude-opus-5", + "us.anthropic.claude-opus-5", + "claude-sonnet-5@default", + "claude-mythos-5-1", + "claude-mythos-5.1", + "claude-5.1-mythos", + "global.anthropic.claude-mythos-5-1-v1:0", + "claude-mythos-5-1@default", + "CLAUDE-MYTHOS-5-1", + "claude-future", + ])("leaves thinking unchanged for %s", (id) => { + const model = claude(sdk.npm, id) + expect(ProviderTransform.providerOptions(model, {})).toEqual({ [sdk.key]: {} }) + expect( + ProviderTransform.providerOptions(model, { [sdk.option]: { type: "adaptive", display: "summarized" } }), + ).toEqual({ [sdk.key]: { [sdk.option]: { type: "adaptive", display: "summarized" } } }) + expect( + ProviderTransform.providerOptions(model, { [sdk.option]: { type: "enabled", budgetTokens: 4000 } }), + ).toEqual({ [sdk.key]: { [sdk.option]: { type: "enabled", budgetTokens: 4000 } } }) + }) + + test.each(["claude-fable-5-1", "claude-opus-5"])("honors explicit binding controls for %s", (id) => { + const model = claude(sdk.npm, id) + const options = Object.freeze({ + [sdk.option]: Object.freeze({ type: "adaptive", blockBinding: false }), + }) + expect(ProviderTransform.providerOptions(model, options)).toEqual({ + [sdk.key]: { [sdk.option]: { type: "adaptive" } }, + }) + expect(ProviderTransform.providerOptions(model, { [sdk.option]: { blockBinding: false } })).toEqual({ + [sdk.key]: {}, + }) + const custom = { [sdk.option]: { type: "adaptive", blockBinding: { prefixMismatchBehavior: "error" } } } + expect(ProviderTransform.providerOptions(model, custom)).toEqual({ [sdk.key]: custom }) + }) + + test.each([ + ["claude-opus-5", "default"], + ["claude-opus-5", "high"], + ["claude-sonnet-5", "default"], + ["claude-sonnet-5", "high"], + ["claude-mythos-5-1", "default"], + ["claude-mythos-5-1", "high"], + ["claude-haiku-4-5", "title"], + ])("omits binding from the %s %s request body and betas", async (id, mode) => { + const requests: Request[] = [] + const capture = Object.assign( + async (...args: Parameters) => { + requests.push(new Request(...args)) + return Response.json( + sdk.key === "bedrock" + ? { + output: { message: { role: "assistant", content: [{ text: "ok" }] } }, + stopReason: "end_turn", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + } + : { + type: "message", + id: "msg_1", + model: "test-model", + role: "assistant", + content: [{ type: "text", text: "ok" }], + stop_reason: "end_turn", + usage: { input_tokens: 1, output_tokens: 1 }, + }, + ) + }, + { preconnect: () => undefined }, + ) + const provider = + sdk.key === "bedrock" + ? createAmazonBedrock({ apiKey: "test-key", region: "ap-southeast-1", fetch: capture }) + : sdk.npm === "@ai-sdk/google-vertex/anthropic" + ? createVertexAnthropic({ + project: "test-project", + location: "global", + generateAuthToken: async () => "test-token", + fetch: capture, + }) + : createAnthropic({ apiKey: "test-key", fetch: capture }) + const model = claude( + sdk.npm, + sdk.key === "bedrock" + ? `global.anthropic.${id}` + : sdk.npm === "@ai-sdk/google-vertex/anthropic" + ? `${id}@default` + : id, + ) + const variants = ProviderTransform.variants(model) + const options = + mode === "title" + ? ProviderTransform.smallOptions({ ...model, variants }) + : mode === "high" + ? variants.high + : {} + await generateText({ + model: provider(model.api.id), + prompt: "hi", + maxOutputTokens: 32000, + providerOptions: ProviderTransform.providerOptions(model, options), + }) + expect(requests).toHaveLength(1) + const body = await requests[0].json() + const fields = sdk.key === "bedrock" ? (body.additionalModelRequestFields ?? {}) : body + expect(fields.thinking).toEqual( + mode === "title" + ? { type: "enabled", budget_tokens: 16000 } + : mode === "high" + ? { type: "adaptive", display: "summarized" } + : undefined, + ) + expect(fields.output_config).toEqual(mode === "high" ? { effort: "high" } : undefined) + expect(fields.anthropic_beta ?? []).not.toContain("thinking-binding-controls-2026-08-01") + expect(requests[0].headers.get("anthropic-beta")?.split(",") ?? []).not.toContain( + "thinking-binding-controls-2026-08-01", + ) + }) + }) + }) + + test("leaves disabled thinking alone", () => { + const model = claude("@ai-sdk/anthropic", "claude-fable-5-1") + expect(ProviderTransform.providerOptions(model, { thinking: { type: "disabled" } })).toEqual({ + anthropic: { thinking: { type: "disabled" } }, + }) + }) + + test("applies to vertex anthropic", () => { + const model = claude("@ai-sdk/google-vertex/anthropic", "claude-fable-5-1") + expect(ProviderTransform.providerOptions(model, { thinking: { type: "adaptive" }, effort: "max" })).toEqual({ + anthropic: { thinking: { type: "adaptive", blockBinding: binding }, effort: "max" }, + }) + }) + + test("applies to bedrock reasoningConfig", () => { + const model = claude("@ai-sdk/amazon-bedrock", "us.anthropic.claude-fable-5-1-v1:0") + expect( + ProviderTransform.providerOptions(model, { reasoningConfig: { type: "adaptive", maxReasoningEffort: "high" } }), + ).toEqual({ + bedrock: { reasoningConfig: { type: "adaptive", maxReasoningEffort: "high", blockBinding: binding } }, + }) + expect(ProviderTransform.providerOptions(model, {})).toEqual({ + bedrock: { reasoningConfig: { type: "adaptive", blockBinding: binding } }, + }) + }) + + test("does not touch bedrock non-anthropic models", () => { + const model = claude("@ai-sdk/amazon-bedrock", "amazon.nova-pro-v1:0") + expect(ProviderTransform.providerOptions(model, { reasoningConfig: { type: "enabled" } })).toEqual({ + bedrock: { reasoningConfig: { type: "enabled" } }, + }) + }) + + test("does not touch non-claude models on anthropic-compatible transports", () => { + const model = claude("@ai-sdk/anthropic", "kimi-k2-thinking") + expect(ProviderTransform.providerOptions(model, { thinking: { type: "adaptive" } })).toEqual({ + anthropic: { thinking: { type: "adaptive" } }, + }) + }) + + test("reaches the anthropic wire as block_binding plus beta header", async () => { + const model = claude("@ai-sdk/anthropic", "claude-fable-5-1") + let sent: { headers: Headers; body: any } | undefined + const provider = createAnthropic({ + apiKey: "test-key", + fetch: Object.assign( + async (...args: Parameters) => { + sent = { headers: new Headers(args[1]?.headers), body: JSON.parse(String(args[1]?.body)) } + return Response.json({ + type: "message", + id: "msg_1", + model: "claude-fable-5-1", + role: "assistant", + content: [{ type: "text", text: "ok" }], + stop_reason: "end_turn", + usage: { input_tokens: 1, output_tokens: 1 }, + input_transformations: [ + { type: "thinking_dropped", path: "messages.1.content.0", reason: "prefix_binding_mismatch" }, + ], + }) + }, + { preconnect: () => undefined }, + ), + }) + const result = await generateText({ + model: provider("claude-fable-5-1"), + prompt: "hi", + providerOptions: ProviderTransform.providerOptions(model, {}), + }) + expect(sent?.body.thinking).toEqual({ + type: "adaptive", + block_binding: { prefix_mismatch_behavior: "drop_block" }, + }) + expect(sent?.headers.get("anthropic-beta")?.split(",")).toContain("thinking-binding-controls-2026-08-01") + expect(result.providerMetadata?.anthropic?.inputTransformations).toEqual([ + { type: "thinking_dropped", path: "messages.1.content.0", reason: "prefix_binding_mismatch" }, + ]) + }) + + test("reaches the vertex anthropic wire as block_binding plus beta header", async () => { + const model = claude("@ai-sdk/google-vertex/anthropic", "claude-fable-5-1") + let sent: { url: string; headers: Headers; body: any } | undefined + const provider = createVertexAnthropic({ + project: "test-project", + location: "global", + generateAuthToken: async () => "test-token", + fetch: Object.assign( + async (...args: Parameters) => { + sent = { + url: String(args[0]), + headers: new Headers(args[1]?.headers), + body: JSON.parse(String(args[1]?.body)), + } + return Response.json({ + type: "message", + id: "msg_1", + model: "claude-fable-5-1", + role: "assistant", + content: [{ type: "text", text: "ok" }], + stop_reason: "end_turn", + usage: { input_tokens: 1, output_tokens: 1 }, + }) + }, + { preconnect: () => undefined }, + ), + }) + await generateText({ + model: provider("claude-fable-5-1"), + prompt: "hi", + providerOptions: ProviderTransform.providerOptions(model, {}), + }) + // Same wire shape Anthropic's own Vertex SDK produces: rawPredict URL, model moved + // out of the body, anthropic_version added, betas carried in the anthropic-beta header. + expect(sent?.url).toBe( + "https://aiplatform.googleapis.com/v1/projects/test-project/locations/global/publishers/anthropic/models/claude-fable-5-1:rawPredict", + ) + expect(sent?.body.model).toBeUndefined() + expect(sent?.body.anthropic_version).toBe("vertex-2023-10-16") + expect(sent?.body.thinking).toEqual({ + type: "adaptive", + block_binding: { prefix_mismatch_behavior: "drop_block" }, + }) + expect(sent?.headers.get("anthropic-beta")?.split(",")).toContain("thinking-binding-controls-2026-08-01") + }) + + test("reaches the bedrock wire as additionalModelRequestFields", async () => { + const model = claude("@ai-sdk/amazon-bedrock", "us.anthropic.claude-fable-5-1-v1:0") + let body: any + const provider = createAmazonBedrock({ + apiKey: "test-key", + region: "us-east-1", + fetch: Object.assign( + async (...args: Parameters) => { + body = JSON.parse(String(args[1]?.body)) + return Response.json({ + output: { message: { role: "assistant", content: [{ text: "ok" }] } }, + stopReason: "end_turn", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + }) + }, + { preconnect: () => undefined }, + ), + }) + await generateText({ + model: provider("us.anthropic.claude-fable-5-1-v1:0"), + prompt: "hi", + providerOptions: ProviderTransform.providerOptions(model, { + reasoningConfig: { type: "adaptive", maxReasoningEffort: "high" }, + }), + }) + expect(body.additionalModelRequestFields.thinking).toEqual({ + type: "adaptive", + block_binding: { prefix_mismatch_behavior: "drop_block" }, + }) + expect(body.additionalModelRequestFields.anthropic_beta).toContain("thinking-binding-controls-2026-08-01") + }) + }) + test("forces reasoning for explicit effort even when model is not marked reasoning-capable", () => { const model = createModel({ capabilities: { @@ -2362,6 +2714,134 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => expect(result[1].content[0]).toEqual({ type: "text", text: "Answer" }) }) + describe("Bedrock reasoning replay", () => { + const model = { + ...anthropicModel, + id: "amazon-bedrock/anthropic.claude-opus-4-6", + providerID: "amazon-bedrock", + api: { + id: "anthropic.claude-opus-4-6", + url: "https://bedrock-runtime.us-east-1.amazonaws.com", + npm: "@ai-sdk/amazon-bedrock", + }, + } + + for (const cached of [false, true]) { + test(`omits unsigned reasoning before SDK conversion (caching: ${cached})`, async () => { + const selected = cached + ? model + : { ...model, id: "amazon-bedrock/openai.gpt-oss-120b", api: { ...model.api, id: "openai.gpt-oss-120b" } } + const messages = ProviderTransform.message( + [ + { role: "user", content: "Think" }, + { role: "assistant", content: [{ type: "text", text: "Earlier answer" }] }, + { + role: "assistant", + content: [ + { type: "reasoning", text: "Partial thought" }, + { type: "text", text: "" }, + ], + }, + { role: "user", content: "Continue" }, + ], + selected, + {}, + ) + expect(messages.map((message) => message.role)).toEqual(["user", "assistant", "user"]) + expect(messages[1].providerOptions?.bedrock?.cachePoint).toEqual(cached ? { type: "default" } : undefined) + const provider = createAmazonBedrock({ + apiKey: "test-key", + region: "us-east-1", + fetch: Object.assign( + async (...args: Parameters) => { + const body = JSON.parse(String(args[1]?.body)) + expect(body.messages).toEqual([ + { role: "user", content: [{ text: "Think" }] }, + { + role: "assistant", + content: [{ text: "Earlier answer" }, ...(cached ? [{ cachePoint: { type: "default" } }] : [])], + }, + { + role: "user", + content: [{ text: "Continue" }, ...(cached ? [{ cachePoint: { type: "default" } }] : [])], + }, + ]) + return Response.json({ + output: { message: { role: "assistant", content: [{ text: "Recovered" }] } }, + stopReason: "end_turn", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + }) + }, + { preconnect: () => undefined }, + ), + }) + const result = await generateText({ model: provider(selected.api.id), messages, maxRetries: 0 }) + expect(result.text).toBe("Recovered") + }) + } + + for (const namespace of ["bedrock", "amazon-bedrock", "custom-bedrock"]) { + for (const field of ["signature", "redactedContent", "redactedData"]) { + test(`preserves ${namespace}.${field} on empty reasoning`, () => { + const result = ProviderTransform.message( + [ + { + role: "assistant", + content: [{ type: "reasoning", text: "", providerOptions: { [namespace]: { [field]: "opaque" } } }], + }, + ], + namespace === "custom-bedrock" ? { ...model, providerID: namespace } : model, + {}, + ) + expect(result).toHaveLength(1) + expect(result[0].content).toEqual([ + { type: "reasoning", text: "", providerOptions: { bedrock: { [field]: "opaque" } } }, + ]) + }) + } + } + + test("uses stored provider metadata when it will overwrite the SDK namespace", () => { + const result = ProviderTransform.message( + [ + { + role: "assistant", + content: [ + { + type: "reasoning", + text: "Partial thought", + providerOptions: { "amazon-bedrock": {}, bedrock: { signature: "overwritten" } }, + }, + ], + }, + ], + model, + {}, + ) + expect(result).toEqual([]) + }) + + test("keeps text and tool calls next to unsigned reasoning", () => { + const content: ModelMessage["content"] = [ + { type: "text", text: "Answer" }, + { type: "tool-call", toolCallId: "call_1", toolName: "lookup", input: {} }, + ] + const result = ProviderTransform.message( + [{ role: "assistant", content: [{ type: "reasoning", text: "Partial thought" }, ...content] }], + model, + {}, + ) + expect(result).toHaveLength(1) + expect(result[0].content).toEqual(content) + }) + + test("does not remove unsigned reasoning for other providers", () => { + const content: ModelMessage["content"] = [{ type: "reasoning", text: "Partial thought" }] + const result = ProviderTransform.message([{ role: "assistant", content }], anthropicModel, {}) + expect(result[0].content).toEqual(content) + }) + }) + test("does not filter for non-anthropic providers", () => { const openaiModel = { ...anthropicModel, @@ -3216,6 +3696,19 @@ describe("ProviderTransform.temperature - Cohere North", () => { }) }) +describe("ProviderTransform sampling defaults - Qwen", () => { + test.each(["Qwen3.8-27B", "qwen3-coder-30b-a3b-instruct"])('leaves sampling unset for "%s"', (id) => { + const model = { + id: `custom/${id}`, + api: { id }, + } as any + + expect(ProviderTransform.temperature(model)).toBeUndefined() + expect(ProviderTransform.topP(model)).toBeUndefined() + expect(ProviderTransform.topK(model)).toBeUndefined() + }) +}) + describe("ProviderTransform sampling defaults - Gemini", () => { const model = (id: string) => ({ @@ -3380,6 +3873,42 @@ describe("ProviderTransform.reasoningVariants", () => { ) }) + test.each(["luna", "sol", "terra"])("serializes Bedrock GPT-5.6 %s none effort", async (name) => { + const item = target("@ai-sdk/amazon-bedrock", `global.openai.gpt-5.6-${name}`) + const variants = ProviderTransform.reasoningVariants( + model([{ type: "effort", values: ["none", "low", "medium", "high", "xhigh", "max"] }]), + item, + ) + for (const effort of ["low", "medium", "high", "xhigh", "max"]) { + expect(variants?.[effort]).toEqual({ reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }) + } + expect(variants?.none).toEqual({ + reasoningConfig: { type: "enabled", maxReasoningEffort: "none" }, + } satisfies AmazonBedrockLanguageModelOptions) + const sent: unknown[] = [] + const provider = createAmazonBedrock({ + apiKey: "test-key", + region: "us-east-1", + fetch: Object.assign( + async (...args: Parameters) => { + sent.push(JSON.parse(String(args[1]?.body))) + return Response.json({ + output: { message: { role: "assistant", content: [{ text: "ok" }] } }, + stopReason: "end_turn", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + }) + }, + { preconnect: () => undefined }, + ), + }) + await generateText({ + model: provider(item.api.id), + prompt: "hi", + providerOptions: ProviderTransform.providerOptions(item, variants?.none ?? {}), + }) + expect(sent).toEqual([expect.objectContaining({ additionalModelRequestFields: { reasoning: { effort: "none" } } })]) + }) + test("combines effort with extended thinking for Claude Opus 4.5", () => { expect( ProviderTransform.reasoningVariants( diff --git a/packages/opencode/test/server/httpapi-global.test.ts b/packages/opencode/test/server/httpapi-global.test.ts index 2807505779..c18d291cfc 100644 --- a/packages/opencode/test/server/httpapi-global.test.ts +++ b/packages/opencode/test/server/httpapi-global.test.ts @@ -44,9 +44,12 @@ const apiLayer = HttpRouter.serve( const it = testEffect(apiLayer) describe("global HttpApi", () => { - it.live("uses the identity-specific upgrade behavior when the request body is omitted", () => + it.live("upgrades to the requested version", () => Effect.gen(function* () { - const response = yield* HttpClient.post(GlobalPaths.upgrade) + const response = yield* HttpClientRequest.post(GlobalPaths.upgrade).pipe( + HttpClientRequest.bodyJsonUnsafe({ target: "9.9.9" }), + HttpClient.execute, + ) const body = yield* response.json if (OpenCodezIdentity.enabled) { @@ -62,15 +65,36 @@ describe("global HttpApi", () => { }), ) - it.live("rejects malformed upgrade payloads", () => + it.live("rejects invalid upgrade payloads", () => + Effect.gen(function* () { + const response = yield* HttpClientRequest.post(GlobalPaths.upgrade).pipe( + HttpClientRequest.bodyJsonUnsafe({ target: 1 }), + HttpClient.execute, + ) + + expect(response.status).toBe(400) + }), + ) + + it.live("rejects invalid upgrade target versions", () => Effect.gen(function* () { const response = yield* HttpClientRequest.post(GlobalPaths.upgrade).pipe( - HttpClientRequest.setBody(HttpBody.text("{", "application/json")), + HttpClientRequest.bodyJsonUnsafe({ target: "latest" }), HttpClient.execute, ) expect(response.status).toBe(400) - expect(yield* response.json).toEqual({ success: false, error: "Invalid request body" }) + }), + ) + + it.live("rejects unsupported upgrade content types", () => + Effect.gen(function* () { + const response = yield* HttpClientRequest.post(GlobalPaths.upgrade).pipe( + HttpClientRequest.setBody(HttpBody.text('{"target":"1.0.0"}', "text/plain")), + HttpClient.execute, + ) + + expect(response.status).toBe(415) }), ) }) diff --git a/packages/opencode/test/server/httpapi-v2-pty.test.ts b/packages/opencode/test/server/httpapi-v2-pty.test.ts index ef05dfe1ea..ea4b02dc83 100644 --- a/packages/opencode/test/server/httpapi-v2-pty.test.ts +++ b/packages/opencode/test/server/httpapi-v2-pty.test.ts @@ -81,7 +81,7 @@ describe("v2 pty HttpApi", () => { expect(body.data.title).toBe("v2") // The canonical surface keeps exited sessions observable with their exit code. - const deadline = Date.now() + 5_000 + const deadline = Date.now() + 20_000 let info: { status: string; exitCode?: number } | undefined while (Date.now() < deadline) { const found = await request(`/api/pty/${body.data.id}`, tmp.path) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 129ade4120..6c5c273c39 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -1774,6 +1774,22 @@ describe("SessionNs.getUsage", () => { expect(Number.isNaN(result.cost)).toBe(false) }) + test("ignores malformed cost fields", () => { + const model = createModel({ + context: 100_000, + output: 32_000, + cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } }, + }) + Object.assign(model.cost, { input: {} }) + + const result = SessionNs.getUsage({ + model, + usage: usage({ inputTokens: 1_000_000, outputTokens: 100_000, totalTokens: 1_100_000 }), + }) + + expect(result.cost).toBe(1.5) + }) + test("calculates cost correctly", () => { const model = createModel({ context: 100_000, diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 3bfc722e2b..fcb536f46d 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -27,6 +27,7 @@ import { ModelV2 } from "@opencode-ai/core/model" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" +import { ProviderError } from "@/provider/error" type ConfigModel = NonNullable[string]["models"]>[string] @@ -753,6 +754,75 @@ function createEventResponse(chunks: unknown[], includeDone = false) { describe("session.llm.stream", () => { const vivgridFixture = { providerID: "vivgrid", modelID: "gemini-3.1-pro-preview" } + const opencodeFixture = { providerID: "opencode-test", modelID: vivgridFixture.modelID } + + it.instance( + "sends the parent session header for opencode providers", + () => + Effect.gen(function* () { + const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID) + const request = waitRequest( + "/chat/completions", + new Response(createChatStream("Hello"), { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + }), + ) + const resolved = yield* Provider.use.getModel( + ProviderV2.ID.make(opencodeFixture.providerID), + ModelV2.ID.make(opencodeFixture.modelID), + ) + const sessionID = SessionID.make("session-child") + const parentSessionID = SessionID.make("session-parent") + const agent = { + name: "test", + mode: "primary", + options: {}, + permission: [{ permission: "*", pattern: "*", action: "allow" }], + } satisfies Agent.Info + const user = { + id: MessageID.make("msg_user-parent-header"), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: agent.name, + model: { + providerID: ProviderV2.ID.make(opencodeFixture.providerID), + modelID: resolved.id, + }, + } satisfies SessionV1.User + + yield* drain({ + user, + sessionID, + parentSessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [{ role: "user", content: "Hello" }], + tools: {}, + }) + + expect((yield* Effect.promise(() => request)).headers.get("x-parent-session-id")).toBe(parentSessionID) + }), + { + config: () => { + const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID) + return { + enabled_providers: [opencodeFixture.providerID], + provider: { + [opencodeFixture.providerID]: { + name: "OpenCode Test", + npm: "@ai-sdk/openai-compatible", + models: { [fixture.model.id]: configModel(fixture.model) as ConfigModel }, + options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` }, + }, + }, + } + }, + }, + ) + it.instance( "sends temperature, tokens, and reasoning options for openai-compatible models", () => @@ -832,6 +902,70 @@ describe("session.llm.stream", () => { }, ) + it.instance( + "surfaces network_error finish reasons as retryable stream failures", + () => + Effect.gen(function* () { + const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID) + const request = waitRequest( + "/chat/completions", + createEventResponse( + [ + { + id: "chatcmpl-network-error", + object: "chat.completion.chunk", + choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: "network_error" }], + }, + ], + true, + ), + ) + const resolved = yield* Provider.use.getModel( + ProviderV2.ID.make(vivgridFixture.providerID), + ModelV2.ID.make(fixture.model.id), + ) + const sessionID = SessionID.make("session-test-network-error") + const agent = { + name: "test", + mode: "primary", + options: {}, + permission: [{ permission: "*", pattern: "*", action: "allow" }], + } satisfies Agent.Info + const user = { + id: MessageID.make("msg_user-network-error"), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: agent.name, + model: { providerID: ProviderV2.ID.make(vivgridFixture.providerID), modelID: resolved.id }, + } satisfies SessionV1.User + + const error = yield* drain({ + user, + sessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [{ role: "user", content: "Hello" }], + tools: {}, + }).pipe(Effect.flip) + yield* Effect.promise(() => request) + + if (!(error instanceof ProviderError.ResponseStreamError)) throw error + expect(error.message).toBe("Provider finish_reason: network_error") + }), + { + config: () => ({ + enabled_providers: [vivgridFixture.providerID], + provider: { + [vivgridFixture.providerID]: { + options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` }, + }, + }, + }), + }, + ) + const cerebrasFixture = { providerID: "cerebras", modelID: "gpt-oss-120b" } it.instance( "replays Cerebras assistant reasoning using the provider-supported field", diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index b4e7a52773..caf64cca86 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -690,6 +690,63 @@ it.live("session.processor effect tests retry OpenAI-compatible midstream server ), ) +it.live("session.processor effect tests retry network_error finish reasons", () => + provideTmpdirServer( + ({ dir, llm }) => + Effect.gen(function* () { + const { processors, session, provider } = yield* boot() + + yield* llm.push( + raw({ + chunks: [ + { + id: "chatcmpl-network-error", + object: "chat.completion.chunk", + choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: "network_error" }], + }, + ], + }), + ) + yield* llm.text("after retry") + + const chat = yield* session.create({}) + const parent = yield* user(chat.id, "retry network error") + const msg = yield* assistant(chat.id, parent.id, path.resolve(dir)) + const mdl = yield* provider.getModel(ref.providerID, ref.modelID) + const handle = yield* processors.create({ + assistantMessage: msg, + sessionID: chat.id, + model: mdl, + }) + + const value = yield* handle.process({ + user: { + id: parent.id, + sessionID: chat.id, + role: "user", + time: parent.time, + agent: parent.agent, + model: { providerID: ref.providerID, modelID: ref.modelID }, + } satisfies SessionV1.User, + sessionID: chat.id, + model: mdl, + agent: agent(), + system: [], + messages: [{ role: "user", content: "retry network error" }], + tools: {}, + }) + + const parts = yield* MessageV2.parts(msg.id) + + expect(value).toBe("continue") + expect(yield* llm.calls).toBe(2) + expect(parts.some((part) => part.type === "text" && part.text === "after retry")).toBe(true) + expect(handle.message.error).toBeUndefined() + }), + { config: (url) => providerCfg(url) }, + ), +) + it.live("session.processor effect tests publish retry status updates", () => provideTmpdirServer( ({ dir, llm }) => diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 5a0176abc9..da6e0f8d03 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -850,6 +850,34 @@ it.instance("loop continues when finish is tool-calls", () => }), ) +it.instance("loop continues when finish is unknown", () => + Effect.gen(function* () { + const { llm } = yield* useServerConfig(providerCfg) + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const session = yield* sessions.create({ + title: "Pinned", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) + yield* prompt.prompt({ + sessionID: session.id, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "hello" }], + }) + yield* llm.push(reply()) + yield* llm.text("second") + + const result = yield* prompt.loop({ sessionID: session.id }) + expect(yield* llm.calls).toBe(2) + expect(result.info.role).toBe("assistant") + if (result.info.role === "assistant") { + expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true) + expect(result.info.finish).toBe("stop") + } + }), +) + it.instance("glob tool keeps instance context during prompt runs", () => Effect.gen(function* () { const { dir, llm } = yield* useServerConfig(providerCfg) diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index e21b12c089..20c8678cf0 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -207,6 +207,9 @@ describe("session.retry.retryable", () => { "provider-returned-error", "terminated", "fetch failed", + "network error", + "network-error", + "network_error", "connection refused", "connect ECONNREFUSED", "request ETIMEDOUT", @@ -215,6 +218,9 @@ describe("session.retry.retryable", () => { "response timed out", "Please retry your request", "try your request again", + "Please try again in a few minutes", + "The model is currently at capacity due to high demand", + "The service is temporarily at capacity", "upstream returned status 524", ])("retries matching API error text: %s", (message) => { expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message }) @@ -348,7 +354,7 @@ describe("session.retry.retryable", () => { reason: "free_tier_limit", provider: "opencode", title: "Free limit reached", - message: "Subscribe to OpenCode Go for reliable access to the best open-source models, starting at $5/month.", + message: "Subscribe to OpenCode Go for reliable access to the best open-source models for $10/month.", label: "subscribe", link: SessionRetry.GO_UPSELL_URL, }, diff --git a/packages/opencode/test/session/tools.test.ts b/packages/opencode/test/session/tools.test.ts new file mode 100644 index 0000000000..f365f684e6 --- /dev/null +++ b/packages/opencode/test/session/tools.test.ts @@ -0,0 +1,167 @@ +import { expect } from "bun:test" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { SessionV1 } from "@opencode-ai/core/v1/session" +import { Agent } from "@/agent/agent" +import { MCP } from "@/mcp" +import { Permission } from "@/permission" +import { Provider } from "@/provider/provider" +import { Session } from "@/session/session" +import { MessageID, PartID, SessionID } from "@/session/schema" +import { SessionProcessor } from "@/session/processor" +import { SessionTools } from "@/session/tools" +import { Tool } from "@/tool/tool" +import { ToolRegistry } from "@/tool/registry" +import { Truncate } from "@/tool/truncate" +import { Plugin } from "@/plugin" +import { RuntimeFlags } from "@/effect/runtime-flags" +import { Effect, Layer, Schema } from "effect" +import { testEffect } from "../lib/effect" + +const callID = "call-test" +const sessionID = SessionID.make("ses_test") +const messageID = MessageID.ascending() +const partID = PartID.ascending() + +const agent: Agent.Info = { + name: "build", + mode: "primary", + options: {}, + permission: [{ permission: "*", pattern: "*", action: "allow" }], +} + +const model = { + providerID: ProviderV2.ID.make("test"), + api: { id: "test-model" }, +} as Provider.Model + +function fakeMcp() { + return MCP.Service.of({ + tools: () => Effect.succeed({}), + clients: () => Effect.succeed({}), + } as Partial as MCP.Interface) +} + +const fakePlugin = Plugin.Service.of({ + init: () => Effect.void, + list: () => Effect.succeed([]), + trigger: (_name, _input, output) => Effect.succeed(output), +} satisfies Plugin.Interface) + +const fakePermission = Permission.Service.of({ + ask: () => Effect.void, + reply: () => Effect.void, + list: () => Effect.succeed([]), +} satisfies Permission.Interface) + +const fakeTruncate = Truncate.Service.of({ + cleanup: () => Effect.void, + write: () => Effect.succeed("output.txt"), + output: (text: string) => Effect.succeed({ content: text, truncated: false }), + limits: () => Effect.succeed({ maxLines: 2000, maxBytes: 50 * 1024 }), +} satisfies Truncate.Interface) + +const layer = Layer.mergeAll( + Layer.succeed(Plugin.Service, fakePlugin), + Layer.succeed(Permission.Service, fakePermission), + Layer.succeed(MCP.Service, fakeMcp()), + Layer.succeed(Truncate.Service, fakeTruncate), + RuntimeFlags.layer(), + Layer.succeed( + ToolRegistry.Service, + ToolRegistry.Service.of({ + ids: () => Effect.succeed(["timing"]), + all: () => Effect.succeed([]), + named: () => Effect.die("unused"), + tools: () => + Effect.succeed([ + { + id: "timing", + description: "updates metadata more than once", + parameters: Schema.Struct({}), + jsonSchema: { type: "object", properties: {} }, + execute: (_args, ctx) => + Effect.gen(function* () { + yield* ctx.metadata({ metadata: { output: "first" } }) + yield* ctx.metadata({ metadata: { output: "second" } }) + return { title: "timing", metadata: {}, output: "done" } + }), + } satisfies Tool.Def, + ]), + }), + ), +) + +const it = testEffect(layer) + +it.effect("preserves running tool start time across metadata updates", () => + Effect.gen(function* () { + const state: SessionV1.ToolPart = { + id: partID, + sessionID, + messageID, + type: "tool", + tool: "timing", + callID, + state: { + status: "running", + input: {}, + time: { start: 100 }, + }, + } + const updates: number[] = [] + const processor = { + message: { + id: messageID, + sessionID, + role: "assistant", + parentID: MessageID.ascending(), + agent: "build", + mode: "build", + path: { cwd: "/tmp", root: "/tmp" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: ModelV2.ID.make("test-model"), + providerID: ProviderV2.ID.make("test"), + time: { created: 1 }, + } satisfies SessionV1.Assistant, + updateToolCall: (_toolCallID, update) => + Effect.sync(() => { + const next = update(state) + state.state = next.state + if (state.state.status === "running") updates.push(state.state.time.start) + return state + }), + completeToolCall: () => Effect.void, + } satisfies Pick + + const tools = yield* SessionTools.resolve({ + agent, + model, + session: { id: sessionID, permission: [] } as unknown as Session.Info, + processor, + bypassAgentCheck: false, + messages: [], + promptOps: {} as never, + }) + const execute = tools.timing.execute + if (!execute) throw new Error("timing tool is missing execute") + + yield* Effect.promise(() => + execute( + {}, + { + toolCallId: callID, + abortSignal: new AbortController().signal, + messages: [], + }, + ), + ) + + expect(updates).toEqual([100, 100]) + expect(state.state.status).toBe("running") + if (state.state.status === "running") { + expect(state.state.time.start).toBe(100) + } + }), +) diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index e394d8084f..742036154b 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -1,8 +1,9 @@ import { describe, expect } from "bun:test" import path from "path" import * as fs from "fs/promises" +import { PermissionV1 } from "@opencode-ai/core/v1/permission" import { LayerNode } from "@opencode-ai/core/effect/layer-node" -import { Cause, Effect, Exit, Layer } from "effect" +import { Cause, Effect, Exit, Layer, Schema } from "effect" import { ApplyPatchTool } from "../../src/tool/apply_patch" import { LSP } from "@/lsp/lsp" import { FSUtil } from "@opencode-ai/core/fs-util" @@ -107,6 +108,25 @@ describe("tool.apply_patch freeform", () => { }), ) + it.instance( + "produces JSON-encodable permission metadata", + () => + Effect.gen(function* () { + const { ctx, calls } = makeCtx() + yield* execute({ patchText: "*** Begin Patch\n*** Add File: new.txt\n+created\n*** End Patch" }, ctx) + + expect(() => { + const request = Schema.encodeUnknownSync(PermissionV1.Request)({ + id: PermissionV1.ID.ascending(), + sessionID: baseCtx.sessionID, + ...calls[0], + }) + Schema.encodeUnknownSync(Schema.Json)(request) + }).not.toThrow() + }), + { git: true }, + ) + it.instance( "applies add/update/delete in one patch", () => diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 2bcf05a2a4..42f46fd35d 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -3,7 +3,7 @@ import { SessionV1 } from "@opencode-ai/core/v1/session" import { Database } from "@opencode-ai/core/database/database" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { SessionProjector } from "@opencode-ai/core/session/projector" -import { Deferred, Effect, Exit, Fiber, Layer } from "effect" +import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect" import { Agent } from "../../src/agent/agent" import { BackgroundJob } from "@/background/job" import { EventV2Bridge } from "@/event-v2-bridge" @@ -96,19 +96,29 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") { return { chat, assistant } }) -function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void; text?: string }): TaskPromptOps { +function stubOps(opts?: { + onPrompt?: (input: SessionPrompt.PromptInput) => void + text?: string + error?: NonNullable + toolError?: string +}): TaskPromptOps { return { cancel: () => Effect.void, resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]), prompt: (input) => Effect.sync(() => { opts?.onPrompt?.(input) - return reply(input, opts?.text ?? "done") + return reply(input, opts?.text ?? "done", opts?.error, opts?.toolError) }), } } -function reply(input: SessionPrompt.PromptInput, text: string): SessionV1.WithParts { +function reply( + input: SessionPrompt.PromptInput, + text: string, + error?: NonNullable, + toolError?: string, +): SessionV1.WithParts { const id = MessageID.ascending() return { info: { @@ -125,6 +135,7 @@ function reply(input: SessionPrompt.PromptInput, text: string): SessionV1.WithPa providerID: input.model?.providerID ?? ref.providerID, time: { created: Date.now() }, finish: "stop", + error, }, parts: [ { @@ -134,6 +145,24 @@ function reply(input: SessionPrompt.PromptInput, text: string): SessionV1.WithPa type: "text", text, }, + ...(toolError + ? [ + { + id: PartID.ascending(), + messageID: id, + sessionID: input.sessionID, + type: "tool" as const, + tool: "read", + callID: "call-1", + state: { + status: "error" as const, + input: { filePath: "/external" }, + error: toolError, + time: { start: Date.now(), end: Date.now() }, + }, + }, + ] + : []), ], } } @@ -255,6 +284,93 @@ describe("tool.task", () => { }), ) + it.instance("execute surfaces child errors with a resumable task_id", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* tool.init() + + const exit = yield* def + .execute( + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { + promptOps: stubOps({ + text: "", + error: new SessionV1.APIError({ message: "Network connection lost", isRetryable: false }).toObject(), + }), + }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + .pipe(Effect.exit) + + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isSuccess(exit)) throw new Error("expected task failure") + const child = (yield* sessions.children(chat.id))[0] + expect(child).toBeDefined() + const failure = Cause.squash(exit.cause) + expect(failure).toBeInstanceOf(Error) + if (!(failure instanceof Error)) throw new Error("expected Error defect") + expect(failure.message).toBe(`Subagent failed (task_id: ${child?.id}): Network connection lost`) + }), + ) + + it.instance("execute surfaces terminal child tool errors with a resumable task_id", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* tool.init() + + const exit = yield* def + .execute( + { + description: "inspect external directory", + prompt: "read the external directory", + subagent_type: "general", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { + promptOps: stubOps({ + text: "I will inspect the directory.", + toolError: "The user rejected permission to use this specific tool call.", + }), + }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + .pipe(Effect.exit) + + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isSuccess(exit)) throw new Error("expected task failure") + const child = (yield* sessions.children(chat.id))[0] + const failure = Cause.squash(exit.cause) + expect(failure).toBeInstanceOf(Error) + if (!(failure instanceof Error)) throw new Error("expected Error defect") + expect(failure.message).toBe( + `Subagent failed (task_id: ${child?.id}): The user rejected permission to use this specific tool call.`, + ) + }), + ) + it.instance("execute asks by default and skips checks when bypassed", () => Effect.gen(function* () { const { chat, assistant } = yield* seed() diff --git a/packages/opencode/test/tool/websearch.test.ts b/packages/opencode/test/tool/websearch.test.ts index 349606dec7..fd5849b790 100644 --- a/packages/opencode/test/tool/websearch.test.ts +++ b/packages/opencode/test/tool/websearch.test.ts @@ -37,8 +37,9 @@ describe("websearch provider", () => { expect(selectWebSearchProvider(SESSION_ID, { exa: false, parallel: true })).toBe("parallel") }) - test("is only enabled for opencode or explicit websearch provider flags", () => { + test("is enabled for OpenCode providers or explicit websearch provider flags", () => { expect(webSearchEnabled(ProviderV2.ID.opencode, { exa: false, parallel: false })).toBe(true) + expect(webSearchEnabled(ProviderV2.ID.make("opencode-go"), { exa: false, parallel: false })).toBe(true) expect(webSearchEnabled(ProviderV2.ID.openai, { exa: false, parallel: false })).toBe(false) expect(webSearchEnabled(ProviderV2.ID.openai, { exa: true, parallel: false })).toBe(true) expect(webSearchEnabled(ProviderV2.ID.openai, { exa: false, parallel: true })).toBe(true) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 29b9c93ed9..6af058c720 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.18.18", + "version": "1.18.29", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 06f5889604..e6fa0addcc 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.18.18", + "version": "1.18.29", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 481284ea5d..d4d77a3326 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -1362,7 +1362,7 @@ export class Global extends HeyApiClient { /** * Upgrade opencode * - * Upgrade opencode to the specified version or latest if not specified. + * Upgrade opencode to the specified version. */ public upgrade( parameters?: { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 471ba2881d..75b56879ae 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1768,11 +1768,14 @@ export type ProviderConfig = { */ timeout?: number | false /** - * Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout. + * Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout. */ headerTimeout?: number | false - chunkTimeout?: number - [key: string]: unknown | string | boolean | number | false | number | false | number | undefined + /** + * Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout. + */ + chunkTimeout?: number | false + [key: string]: unknown | string | boolean | number | false | number | false | number | false | undefined } models?: { [key: string]: { @@ -1952,6 +1955,7 @@ export type Config = { [key: string]: string } wire?: "legacy" | "codex" + context_window?: number compaction?: { threshold?: number token_limit?: number @@ -7413,7 +7417,7 @@ export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeRe export type GlobalUpgradeData = { body?: { - target?: string + target: string } path?: never query?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 98c59fd023..a099ea6336 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -548,7 +548,7 @@ } } }, - "description": "Upgrade opencode to the specified version or latest if not specified.", + "description": "Upgrade opencode to the specified version.", "summary": "Upgrade opencode", "requestBody": { "content": { @@ -560,6 +560,7 @@ "type": "string" } }, + "required": ["target"], "additionalProperties": false } } @@ -21048,11 +21049,20 @@ "enum": [false] } ], - "description": "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout." + "description": "Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout." }, "chunkTimeout": { - "type": "integer", - "exclusiveMinimum": 0 + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0 + }, + { + "type": "boolean", + "enum": [false] + } + ], + "description": "Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout." } }, "additionalProperties": {} diff --git a/packages/server/package.json b/packages/server/package.json index 83eca9036b..3c016338dc 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.18.18", + "version": "1.18.29", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 329a9406c3..0c1c656bb5 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.18.18", + "version": "1.18.29", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/src/v2/components/prompt-input/interaction.ts b/packages/session-ui/src/v2/components/prompt-input/interaction.ts index f4a9fa74b7..dc99a97a91 100644 --- a/packages/session-ui/src/v2/components/prompt-input/interaction.ts +++ b/packages/session-ui/src/v2/components/prompt-input/interaction.ts @@ -377,7 +377,7 @@ export function createPromptInputV2Controller(input: { attachments && (Array.from(clipboard?.items ?? []).some((item) => item.kind === "file") || !clipboard?.getData("text/plain")) ) { - void attachments.handlePaste(event) + void attachments.handlePaste(event).catch(input.attachments!.onError) return } input.view.onPaste?.(event) diff --git a/packages/slack/package.json b/packages/slack/package.json index 3a9cd6b86f..deb856fd5c 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.18.18", + "version": "1.18.29", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index 8da5bda19d..5340918954 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.18.18", + "version": "1.18.29", "private": true, "type": "module", "license": "MIT", @@ -19,25 +19,18 @@ "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", "@solidjs/start": "catalog:", - "d3-geo": "3.1.1", "d3-scale": "4.0.2", "effect": "catalog:", "i18n-iso-countries": "7.14.0", "nitro": "3.0.1-alpha.1", "solid-js": "catalog:", "sst": "catalog:", - "topojson-client": "3.1.0", - "vite": "catalog:", - "world-atlas": "2.0.2" + "vite": "catalog:" }, "devDependencies": { "@cloudflare/workers-types": "catalog:", "@types/bun": "catalog:", - "@types/d3-geo": "3.1.0", "@types/d3-scale": "4.0.9", - "@types/geojson": "7946.0.16", - "@types/topojson-client": "3.1.5", - "@types/topojson-specification": "1.0.5", "@typescript/native-preview": "catalog:", "typescript": "catalog:" }, diff --git a/packages/stats/app/src/component/model-compare-detail.tsx b/packages/stats/app/src/component/model-compare-detail.tsx index 3790789b58..e4c2ac5cbd 100644 --- a/packages/stats/app/src/component/model-compare-detail.tsx +++ b/packages/stats/app/src/component/model-compare-detail.tsx @@ -3,10 +3,10 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { getStatsModelsComparisonData, type ModelUsagePoint, + type RetentionEntry, type StatsModelComparisonInput, type StatsModelComparisonEntry, } from "@opencode-ai/stats-core/domain/home" -import { runtime } from "@opencode-ai/stats-core/runtime" import { createAsync, query, useParams, useSearchParams } from "@solidjs/router" import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js" import { getRequestEvent } from "solid-js/web" @@ -24,6 +24,8 @@ import { findModelCatalogEntry, formatCatalogLabName, getModelCatalog, + isKnownCatalogLab, + isProviderlessLab, type ModelCatalog, type ModelCatalogEntry, } from "../routes/model-catalog" @@ -45,6 +47,7 @@ import { type ResolvedComparisonFamily, } from "../lib/comparison-pages" import { baseUrl } from "../lib/language" +import { runStatsEffect } from "../stats-runtime" const compareHeaderLinks: readonly HeaderLink[] = [ { href: `${import.meta.env.BASE_URL}#top-models`, label: "Top Models" }, @@ -70,7 +73,7 @@ const comparisonModelLimit = 6 type ComparisonModel = { name: string lab: string - labName: string + labName?: string slug: string catalog: ModelCatalogEntry | null stats: StatsModelComparisonEntry | null @@ -109,7 +112,7 @@ export type ModelCompareDetailPageProps = { const getComparisonData = query(async (models: StatsModelComparisonInput[]) => { "use server" - return runtime.runPromise(getStatsModelsComparisonData(models)) + return runStatsEffect(getStatsModelsComparisonData(models)) }, "getStatsModelComparisonDetailData") export default function ModelCompareDetailPage(props: ModelCompareDetailPageProps = {}) { @@ -158,13 +161,19 @@ export default function ModelCompareDetailPage(props: ModelCompareDetailPageProp let comparisonBodyScroll: HTMLDivElement | undefined const models = createMemo(() => modelSelections().map((model, index) => - buildComparisonModel(model.lab, model.slug, model.catalog ?? null, stats()?.models[index] ?? null), + buildComparisonModel( + model.lab, + model.slug, + model.catalog ?? null, + stats()?.models[index] ?? null, + catalog()?.labs.map((lab) => lab.id) ?? [], + ), ), ) const title = createMemo(() => `${models()[0].name} vs ${models()[1].name} - AI Model Comparison`) const description = createMemo( () => - `Compare ${models()[0].name} from ${models()[0].labName} and ${models()[1].name} from ${models()[1].labName} on key metrics including benchmarks, price, context length, usage, and model features.`, + `Compare ${comparisonModelLabel(models()[0])} and ${comparisonModelLabel(models()[1])} on key metrics including benchmarks, price, context length, usage, and model features.`, ) const canonicalPath = createMemo(() => { if (props.family) return canonicalFamilyComparisonPath(props.family.first, props.family.second) @@ -205,7 +214,7 @@ export default function ModelCompareDetailPage(props: ModelCompareDetailPageProp "@type": "SoftwareApplication", name: model.name, applicationCategory: "AI model", - provider: model.labName, + ...(model.labName ? { provider: model.labName } : {}), })), }), ) @@ -458,7 +467,9 @@ function CompareDetailSelectButton(props: { aria-expanded={props.expanded} onClick={props.onOpen} > - + + {(labName) => } + {props.model.name} @@ -579,10 +590,7 @@ function CompareModelDetail(props: { model: ModelCatalogEntry }) { {props.model.name}
    -

    - {props.model.description ?? - `${props.model.name} is an AI model from ${formatCatalogLabName(props.model.lab)}.`} -

    +

    {props.model.description ?? `${props.model.name} is an AI model.`}

    @@ -788,9 +796,11 @@ function LabLogo(props: { lab: string; label: string; size: "large" | "small" | const iconId = () => getProviderIconId(props.lab) return ( - - + + + + ) } @@ -831,11 +841,13 @@ function buildComparisonModel( modelParam: string, catalog: ModelCatalogEntry | null, stats: StatsModelComparisonEntry | null, + catalogLabs: readonly string[], ): ComparisonModel { + const lab = catalog?.lab ?? stats?.provider ?? catalogSlug(labParam) return { name: catalog?.name ?? stats?.model ?? formatParamName(modelParam), - lab: catalog?.lab ?? stats?.provider ?? catalogSlug(labParam), - labName: formatCatalogLabName(catalog?.lab ?? stats?.provider ?? labParam), + lab, + labName: isKnownCatalogLab(lab, catalogLabs) ? formatCatalogLabName(lab) : undefined, slug: catalog?.slug ?? stats?.slug ?? catalogSlug(modelParam), catalog, stats, @@ -874,10 +886,7 @@ function buildComparisonDetailSections(models: readonly ComparisonModel[]): Comp { title: "Overview", rows: [ - comparisonDetailRow( - "Author", - models.map((model) => linkedTextCell(model.stats?.author ?? model.labName, labHref(model.lab))), - ), + comparisonDetailRow("Author", models.map(providerDetailCell)), comparisonDetailRow( "Context length", models.map((model) => limitCell(model.catalog?.limit?.context)), @@ -896,10 +905,7 @@ function buildComparisonDetailSections(models: readonly ComparisonModel[]): Comp "Output modalities", models.map((model) => textCell(formatCatalogModalities(model.catalog?.modalities.output ?? []))), ), - comparisonDetailRow( - "Providers", - models.map((model) => linkedTextCell(model.labName, labHref(model.lab))), - ), + comparisonDetailRow("Providers", models.map(providerDetailCell)), ], }, { @@ -949,6 +955,17 @@ function buildComparisonDetailSections(models: readonly ComparisonModel[]): Comp ], usage: models.map((model) => model.stats?.usage ?? []), }, + { + title: "Retention", + badge: "Week 1", + rows: [ + comparisonDetailRow( + "Returning users", + models.map((model) => retentionCell(model.stats?.weeklyRetention)), + "higher", + ), + ], + }, ] } @@ -1001,6 +1018,15 @@ function comparisonRef(model: ComparisonModel): ComparisonModelRef { } } +function comparisonModelLabel(model: ComparisonModel) { + return model.labName ? `${model.name} from ${model.labName}` : model.name +} + +function providerDetailCell(model: ComparisonModel): ComparisonDetailCell { + if (!model.labName) return textCell("") + return linkedTextCell(model.stats?.author ?? model.labName ?? "", labHref(model.lab)) +} + function textCell(value: string): ComparisonDetailCell { return { value } } @@ -1031,6 +1057,15 @@ function percentCell(value: number | undefined): ComparisonDetailCell { return value === undefined ? { value: "No usage" } : { value: formatPercent(value), score: value } } +function retentionCell(value: RetentionEntry | null | undefined): ComparisonDetailCell { + if (!value || value.rank === null) return { value: "Pending" } + return { + value: formatPercent(value.rate), + unit: `${formatTokens(value.eligibleUserWeeks)} user-weeks`, + score: value.rate, + } +} + function tokenCell(value: number | undefined, trend: number | undefined): ComparisonDetailCell { if (value === undefined) return { value: "No usage" } return { value: formatTokens(value), score: value, trend } diff --git a/packages/stats/app/src/i18n.ts b/packages/stats/app/src/i18n.ts index abb9ef49e3..0b8053694e 100644 --- a/packages/stats/app/src/i18n.ts +++ b/packages/stats/app/src/i18n.ts @@ -20,7 +20,7 @@ import { dict as zht } from "./i18n/zht" const en = { "app.title": "AI Model Usage Rankings | OpenCode Data", "app.description": - "Explore OpenCode Go usage across AI models, including token volume, rankings, market share, token pricing, session cost, cache ratio, and geo breakdowns.", + "Explore OpenCode usage across AI models, including token volume, rankings, market share, token pricing, session cost, cache ratio, and geo breakdowns.", "app.unfurlAlt": "OpenCode Data wordmark on a dark patterned background", "nav.dataHome": "Data Home", "nav.topModels": "Top Models", @@ -104,7 +104,7 @@ const en = { "home.loadingTitle": "Loading data", "home.loadingDescription": "Reading model aggregates.", "home.usageTitle": "Usage", - "home.topModelsDescription": "Usage of models across OpenCode Go.", + "home.topModelsDescription": "Usage of models across OpenCode.", "home.noUsageTitle": "No usage data", "home.noUsageDescription": "No model rows matched this product and range.", "home.noLeaderboardTitle": "No leaderboard data", @@ -116,7 +116,7 @@ const en = { "home.modelUsers": "model users", "home.new": "New", "home.uniqueUsersTitle": "Unique Users", - "home.uniqueUsersDescription": "Daily unique OpenCode Go users by model.", + "home.uniqueUsersDescription": "Daily unique OpenCode users by model.", "home.noUserDataTitle": "No user data", "home.noUserDataDescription": "No user-bearing model rows matched this window.", "home.uniqueUsersChart": "Stacked unique user chart by model", @@ -126,12 +126,9 @@ const en = { "home.noMarketDescription": "No model rows matched this range.", "home.marketChart": "Market share by model author", "home.noData": "No data", - "home.geoTitle": "Geo Breakdown", - "home.geoDescription": "Tokens used by country.", + "home.geoTitle": "Geographic Breakdown", "home.noGeoTitle": "No geo data", "home.noGeoDescription": "No geo rows matched this range.", - "home.worldMap": "World map of token usage by country", - "home.geoMapTitle": "Geo Breakdown map", "home.unknown": "Unknown", "home.tokenCostTitle": "Token Cost", "home.tokenCostDescription": "Price per 1M tokens.", @@ -148,7 +145,7 @@ const en = { "home.costSession": "Cost/Session", "lab.title": "{{lab}} AI Model Usage & Rankings | OpenCode Data", "lab.description": - "Compare {{lab}} models used in OpenCode Go, including token usage, model rankings, context windows, release dates, costs, and model-specific data.", + "Compare {{lab}} models used in OpenCode, including token usage, model rankings, context windows, release dates, costs, and model-specific data.", "lab.loadingTitle": "Model Lab", "lab.loadingDescription": "Reading model availability and recent OpenCode usage.", "lab.notFound": "No models matched this lab.", @@ -174,7 +171,7 @@ const en = { "lab.share": "Share", "model.title": "{{model}} Usage, Cost & Rank | OpenCode Data", "model.description": - "View {{model}} OpenCode Go usage data, including token volume, weekly rank, token mix, costs, cache ratio, sessions, geo breakdowns, and peer models.", + "View {{model}} OpenCode usage data, including token volume, weekly rank, token mix, costs, cache ratio, sessions, geo breakdowns, and peer models.", "model.loadingTitle": "Model Data", "model.loadingDescription": "Reading model aggregates.", "model.loadingProfile": "Reading the model profile.", @@ -183,15 +180,15 @@ const en = { "model.noMatched": "No model facts or usage rows matched {{id}}.", "model.fallback": "Model", "model.catalogFallback": - "Model facts from the shared model index. OpenCode Go usage appears once this model has activity.", - "model.unranked": "Unranked across last week's OpenCode Go usage", - "model.ranked": "Ranked #{{rank}} across last week's OpenCode Go usage", + "Model facts from the shared model index. OpenCode usage appears once this model has activity.", + "model.unranked": "Unranked across last week's OpenCode usage", + "model.ranked": "Ranked #{{rank}} across last week's OpenCode usage", "model.observedVolume": "with {{share}} of observed 2M volume.", "model.weights": "Model weights: {{label}}", "model.rank": "7D Rank", "model.profile": "Model Profile", "model.listed": "Listed", - "model.noCurrentUsage": "No OpenCode Go usage in the current data window.", + "model.noCurrentUsage": "No OpenCode usage in the current data window.", "model.facts": "Model facts", "model.context": "Context", "model.output": "Output", @@ -200,7 +197,7 @@ const en = { "model.inputs": "Inputs", "model.overviewDescription": "Recent tokens, unique users, and market position.", "model.noSummaryTitle": "No usage summary", - "model.noSummaryDescription": "This model has no OpenCode Go usage rows yet.", + "model.noSummaryDescription": "This model has no OpenCode usage rows yet.", "model.tokens": "Tokens", "model.uniqueUsers": "Unique Users", "model.lastTwoMonths": "last two months", @@ -236,11 +233,9 @@ const en = { "model.averageTokensSession": "Average tokens / session", "model.cacheRatio": "Cache Ratio", "model.inputTokens": "input tokens", - "model.geoDescription": "OpenCode Go model tokens used by country.", "model.noGeoTitle": "No geo data", - "model.noGeoDescription": "No OpenCode Go geo rows matched this model.", - "model.worldMap": "World map of model token usage by country", - "model.peersDescription": "Nearby models by recent OpenCode Go token volume.", + "model.noGeoDescription": "No OpenCode geo rows matched this model.", + "model.peersDescription": "Nearby models by recent OpenCode token volume.", "model.noPeersTitle": "No peers", "model.noPeersDescription": "Peer rankings appear after usage lands.", "model.noUsageLastWeek": "No usage last week", diff --git a/packages/stats/app/src/i18n/ar.ts b/packages/stats/app/src/i18n/ar.ts index b46bf14243..b243043102 100644 --- a/packages/stats/app/src/i18n/ar.ts +++ b/packages/stats/app/src/i18n/ar.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "ترتيب استخدام نماذج الذكاء الاصطناعي | OpenCode Data", "app.description": - "استكشف استخدام OpenCode Go عبر نماذج الذكاء الاصطناعي، بما في ذلك حجم الرموز، والترتيب، وحصة السوق، وتسعير الرموز، وتكلفة الجلسة، ونسبة التخزين المؤقت، والتوزيع الجغرافي.", + "استكشف استخدام OpenCode عبر نماذج الذكاء الاصطناعي، بما في ذلك حجم الرموز، والترتيب، وحصة السوق، وتسعير الرموز، وتكلفة الجلسة، ونسبة التخزين المؤقت، والتوزيع الجغرافي.", "app.unfurlAlt": "شعار OpenCode Data على خلفية داكنة مزخرفة", "nav.dataHome": "صفحة البيانات الرئيسية", "nav.topModels": "أهم النماذج", @@ -85,7 +85,7 @@ export const dict = { "home.loadingTitle": "تحميل البيانات", "home.loadingDescription": "قراءة تجميعات النماذج.", "home.usageTitle": "الاستخدام", - "home.topModelsDescription": "استخدام النماذج عبر OpenCode Go.", + "home.topModelsDescription": "استخدام النماذج عبر OpenCode.", "home.noUsageTitle": "لا توجد بيانات استخدام", "home.noUsageDescription": "لم تطابق أي صفوف نماذج هذا المنتج وهذا النطاق.", "home.noLeaderboardTitle": "لا توجد بيانات ترتيب", @@ -97,7 +97,7 @@ export const dict = { "home.modelUsers": "مستخدمو النموذج", "home.new": "جديد", "home.uniqueUsersTitle": "المستخدمون الفريدون", - "home.uniqueUsersDescription": "المستخدمون اليوميون الفريدون لـ OpenCode Go حسب النموذج.", + "home.uniqueUsersDescription": "المستخدمون اليوميون الفريدون لـ OpenCode حسب النموذج.", "home.noUserDataTitle": "لا توجد بيانات مستخدمين", "home.noUserDataDescription": "لم تطابق أي صفوف نماذج تحتوي على مستخدمين هذه النافذة.", "home.uniqueUsersChart": "مخطط مكدس للمستخدمين الفريدين حسب النموذج", @@ -108,11 +108,8 @@ export const dict = { "home.marketChart": "حصة السوق حسب مؤلف النموذج", "home.noData": "لا توجد بيانات", "home.geoTitle": "التوزيع الجغرافي", - "home.geoDescription": "الرموز المستخدمة حسب البلد.", "home.noGeoTitle": "لا توجد بيانات جغرافية", "home.noGeoDescription": "لم تطابق أي صفوف جغرافية هذا النطاق.", - "home.worldMap": "خريطة عالمية لاستخدام الرموز حسب البلد", - "home.geoMapTitle": "خريطة التوزيع الجغرافي", "home.unknown": "غير معروف", "home.tokenCostTitle": "تكلفة الرموز", "home.tokenCostDescription": "السعر لكل مليون رمز.", @@ -129,7 +126,7 @@ export const dict = { "home.costSession": "التكلفة/الجلسة", "lab.title": "استخدام وترتيب نماذج {{lab}} للذكاء الاصطناعي | OpenCode Data", "lab.description": - "قارن نماذج {{lab}} المستخدمة في OpenCode Go، بما في ذلك استخدام الرموز، وترتيب النماذج، ونوافذ السياق، وتواريخ الإصدار، والتكاليف، والبيانات الخاصة بكل نموذج.", + "قارن نماذج {{lab}} المستخدمة في OpenCode، بما في ذلك استخدام الرموز، وترتيب النماذج، ونوافذ السياق، وتواريخ الإصدار، والتكاليف، والبيانات الخاصة بكل نموذج.", "lab.loadingTitle": "مختبر النماذج", "lab.loadingDescription": "قراءة توفر النماذج واستخدام OpenCode الأخير.", "lab.notFound": "لم تطابق أي نماذج هذا المختبر.", @@ -155,7 +152,7 @@ export const dict = { "lab.share": "الحصة", "model.title": "استخدام {{model}} وتكلفته وترتيبه | OpenCode Data", "model.description": - "اعرض بيانات استخدام {{model}} في OpenCode Go، بما في ذلك حجم الرموز، والترتيب الأسبوعي، ومزيج الرموز، والتكاليف، ونسبة التخزين المؤقت، والجلسات، والتوزيع الجغرافي، والنماذج المشابهة.", + "اعرض بيانات استخدام {{model}} في OpenCode، بما في ذلك حجم الرموز، والترتيب الأسبوعي، ومزيج الرموز، والتكاليف، ونسبة التخزين المؤقت، والجلسات، والتوزيع الجغرافي، والنماذج المشابهة.", "model.loadingTitle": "بيانات النموذج", "model.loadingDescription": "قراءة تجميعات النماذج.", "model.loadingProfile": "قراءة ملف النموذج.", @@ -163,25 +160,24 @@ export const dict = { "model.noDataDescription": "جرّب فتح نموذج من لوحة الترتيب.", "model.noMatched": "لم تطابق أي حقائق نموذج أو صفوف استخدام {{id}}.", "model.fallback": "نموذج", - "model.catalogFallback": - "حقائق النموذج من فهرس النماذج المشترك. يظهر استخدام OpenCode Go عندما يصبح لهذا النموذج نشاط.", - "model.unranked": "غير مصنف ضمن استخدام OpenCode Go للأسبوع الماضي", - "model.ranked": "مصنف رقم {{rank}} ضمن استخدام OpenCode Go للأسبوع الماضي", + "model.catalogFallback": "حقائق النموذج من فهرس النماذج المشترك. يظهر استخدام OpenCode عندما يصبح لهذا النموذج نشاط.", + "model.unranked": "غير مصنف ضمن استخدام OpenCode للأسبوع الماضي", + "model.ranked": "مصنف رقم {{rank}} ضمن استخدام OpenCode للأسبوع الماضي", "model.observedVolume": "مع {{share}} من حجم 2M المرصود.", "model.weights": "أوزان النموذج: {{label}}", "model.rank": "ترتيب 7 أيام", "model.profile": "ملف النموذج", "model.listed": "مدرج", - "model.noCurrentUsage": "لا يوجد استخدام لـ OpenCode Go في نافذة البيانات الحالية.", + "model.noCurrentUsage": "لا يوجد استخدام لـ OpenCode في نافذة البيانات الحالية.", "model.facts": "حقائق النموذج", "model.context": "السياق", "model.output": "الإخراج", "model.knowledge": "المعرفة", "model.release": "الإصدار", "model.inputs": "المدخلات", - "model.overviewDescription": "رموز OpenCode Go الأخيرة، والمستخدمون الفريدون، وموقع السوق.", + "model.overviewDescription": "رموز OpenCode الأخيرة، والمستخدمون الفريدون، وموقع السوق.", "model.noSummaryTitle": "لا يوجد ملخص استخدام", - "model.noSummaryDescription": "لا يحتوي هذا النموذج على صفوف استخدام OpenCode Go بعد.", + "model.noSummaryDescription": "لا يحتوي هذا النموذج على صفوف استخدام OpenCode بعد.", "model.tokens": "الرموز", "model.uniqueUsers": "المستخدمون الفريدون", "model.lastTwoMonths": "آخر شهرين", @@ -191,15 +187,15 @@ export const dict = { "model.totalModels": "{{count}} نموذجًا", "model.momentum": "الزخم", "model.vsPreviousWindow": "مقارنة بالنافذة السابقة", - "model.usageDescription": "حجم رموز OpenCode Go اليومي خلال نافذة الشهرين الأخيرة.", + "model.usageDescription": "حجم رموز OpenCode اليومي خلال نافذة الشهرين الأخيرة.", "model.noUsageTitle": "لا يوجد استخدام", "model.noUsageDescription": "لم يصل أي استخدام في النافذة الحالية.", "model.dailyTokenChart": "مخطط استخدام الرموز اليومي", - "model.usersDescription": "المستخدمون اليوميون الفريدون لـ OpenCode Go خلال نافذة الشهرين الأخيرة.", + "model.usersDescription": "المستخدمون اليوميون الفريدون لـ OpenCode خلال نافذة الشهرين الأخيرة.", "model.noUsersTitle": "لا توجد بيانات مستخدمين", "model.noUsersDescription": "لم تصل أي صفوف تحتوي على مستخدمين في النافذة الحالية.", "model.dailyUserChart": "مخطط المستخدمين الفريدين اليومي", - "model.efficiencyDescription": "التكلفة، وسلوك التخزين المؤقت، ومتوسط شكل جلسة OpenCode Go.", + "model.efficiencyDescription": "التكلفة، وسلوك التخزين المؤقت، ومتوسط شكل جلسة OpenCode.", "model.noEfficiencyTitle": "لا توجد بيانات كفاءة", "model.noEfficiencyDescription": "تظهر بيانات الكفاءة بعد وصول الاستخدام.", "model.totalSpendLabel": "Total spend", @@ -217,11 +213,9 @@ export const dict = { "model.tokensSession": "الرموز / الجلسة", "model.cacheRatio": "نسبة التخزين المؤقت", "model.inputTokens": "رموز الإدخال", - "model.geoDescription": "رموز نموذج OpenCode Go المستخدمة حسب البلد.", "model.noGeoTitle": "لا توجد بيانات جغرافية", - "model.noGeoDescription": "لم تطابق أي صفوف جغرافية في OpenCode Go هذا النموذج.", - "model.worldMap": "خريطة عالمية لاستخدام رموز النموذج حسب البلد", - "model.peersDescription": "نماذج قريبة حسب حجم رموز OpenCode Go الأخير.", + "model.noGeoDescription": "لم تطابق أي صفوف جغرافية في OpenCode هذا النموذج.", + "model.peersDescription": "نماذج قريبة حسب حجم رموز OpenCode الأخير.", "model.noPeersTitle": "لا توجد نماذج مشابهة", "model.noPeersDescription": "تظهر ترتيبات النماذج المشابهة بعد وصول الاستخدام.", "model.noUsageLastWeek": "لا يوجد استخدام الأسبوع الماضي", diff --git a/packages/stats/app/src/i18n/br.ts b/packages/stats/app/src/i18n/br.ts index 24941355c5..5bf9e44dc7 100644 --- a/packages/stats/app/src/i18n/br.ts +++ b/packages/stats/app/src/i18n/br.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "Ranking de uso de modelos de IA | OpenCode Data", "app.description": - "Explore o uso do OpenCode Go em modelos de IA, incluindo volume de tokens, rankings, participação de mercado, preço por token, custo por sessão, taxa de cache e distribuição geográfica.", + "Explore o uso do OpenCode em modelos de IA, incluindo volume de tokens, rankings, participação de mercado, preço por token, custo por sessão, taxa de cache e distribuição geográfica.", "app.unfurlAlt": "Logotipo do OpenCode Data sobre um fundo escuro padronizado", "nav.dataHome": "Início dos dados", "nav.topModels": "Principais modelos", @@ -86,7 +86,7 @@ export const dict = { "home.loadingTitle": "Carregando dados", "home.loadingDescription": "Lendo agregados de modelos.", "home.usageTitle": "Uso", - "home.topModelsDescription": "Uso de modelos no OpenCode Go.", + "home.topModelsDescription": "Uso de modelos no OpenCode.", "home.noUsageTitle": "Sem dados de uso", "home.noUsageDescription": "Nenhuma linha de modelo correspondeu a este produto e intervalo.", "home.noLeaderboardTitle": "Sem dados de classificação", @@ -98,7 +98,7 @@ export const dict = { "home.modelUsers": "usuários do modelo", "home.new": "Novo", "home.uniqueUsersTitle": "Usuários únicos", - "home.uniqueUsersDescription": "Usuários únicos diários do OpenCode Go por modelo.", + "home.uniqueUsersDescription": "Usuários únicos diários do OpenCode por modelo.", "home.noUserDataTitle": "Sem dados de usuários", "home.noUserDataDescription": "Nenhuma linha de modelo com usuários correspondeu a esta janela.", "home.uniqueUsersChart": "Gráfico empilhado de usuários únicos por modelo", @@ -109,11 +109,8 @@ export const dict = { "home.marketChart": "Participação de mercado por autor do modelo", "home.noData": "Sem dados", "home.geoTitle": "Distribuição geográfica", - "home.geoDescription": "Tokens usados por país.", "home.noGeoTitle": "Sem dados geográficos", "home.noGeoDescription": "Nenhuma linha geográfica correspondeu a este intervalo.", - "home.worldMap": "Mapa-múndi do uso de tokens por país", - "home.geoMapTitle": "Mapa da distribuição geográfica", "home.unknown": "Desconhecido", "home.tokenCostTitle": "Custo de tokens", "home.tokenCostDescription": "Preço por 1 milhão de tokens.", @@ -130,7 +127,7 @@ export const dict = { "home.costSession": "Custo/sessão", "lab.title": "Uso e ranking de modelos de IA da {{lab}} | OpenCode Data", "lab.description": - "Compare modelos da {{lab}} usados no OpenCode Go, incluindo uso de tokens, ranking de modelos, janelas de contexto, datas de lançamento, custos e dados específicos de cada modelo.", + "Compare modelos da {{lab}} usados no OpenCode, incluindo uso de tokens, ranking de modelos, janelas de contexto, datas de lançamento, custos e dados específicos de cada modelo.", "lab.loadingTitle": "Laboratório de modelos", "lab.loadingDescription": "Lendo disponibilidade de modelos e uso recente do OpenCode.", "lab.notFound": "Nenhum modelo correspondeu a este laboratório.", @@ -157,7 +154,7 @@ export const dict = { "lab.share": "Participação", "model.title": "Uso, custo e posição de {{model}} | OpenCode Data", "model.description": - "Veja dados de uso de {{model}} no OpenCode Go, incluindo volume de tokens, ranking semanal, combinação de tokens, custos, taxa de cache, sessões, distribuição geográfica e modelos pares.", + "Veja dados de uso de {{model}} no OpenCode, incluindo volume de tokens, ranking semanal, combinação de tokens, custos, taxa de cache, sessões, distribuição geográfica e modelos pares.", "model.loadingTitle": "Dados do modelo", "model.loadingDescription": "Lendo agregados de modelos.", "model.loadingProfile": "Lendo o perfil do modelo.", @@ -166,24 +163,24 @@ export const dict = { "model.noMatched": "Nenhum fato do modelo ou linha de uso correspondeu a {{id}}.", "model.fallback": "Modelo", "model.catalogFallback": - "Fatos do modelo do índice compartilhado de modelos. O uso no OpenCode Go aparece quando este modelo tiver atividade.", - "model.unranked": "Sem classificação no uso do OpenCode Go da última semana", - "model.ranked": "Classificado em #{{rank}} no uso do OpenCode Go da última semana", + "Fatos do modelo do índice compartilhado de modelos. O uso no OpenCode aparece quando este modelo tiver atividade.", + "model.unranked": "Sem classificação no uso do OpenCode da última semana", + "model.ranked": "Classificado em #{{rank}} no uso do OpenCode da última semana", "model.observedVolume": "com {{share}} do volume observado de 2M.", "model.weights": "Pesos do modelo: {{label}}", "model.rank": "Ranking 7D", "model.profile": "Perfil do modelo", "model.listed": "Listado", - "model.noCurrentUsage": "Sem uso do OpenCode Go na janela de dados atual.", + "model.noCurrentUsage": "Sem uso do OpenCode na janela de dados atual.", "model.facts": "Fatos do modelo", "model.context": "Contexto", "model.output": "Saída", "model.knowledge": "Conhecimento", "model.release": "Lançamento", "model.inputs": "Entradas", - "model.overviewDescription": "Tokens recentes do OpenCode Go, usuários únicos e posição de mercado.", + "model.overviewDescription": "Tokens recentes do OpenCode, usuários únicos e posição de mercado.", "model.noSummaryTitle": "Sem resumo de uso", - "model.noSummaryDescription": "Este modelo ainda não tem linhas de uso do OpenCode Go.", + "model.noSummaryDescription": "Este modelo ainda não tem linhas de uso do OpenCode.", "model.tokens": "Tokens", "model.uniqueUsers": "Usuários únicos", "model.lastTwoMonths": "últimos dois meses", @@ -193,15 +190,15 @@ export const dict = { "model.totalModels": "{{count}} modelos", "model.momentum": "Momento", "model.vsPreviousWindow": "vs janela anterior", - "model.usageDescription": "Volume diário de tokens do OpenCode Go na janela recente de dois meses.", + "model.usageDescription": "Volume diário de tokens do OpenCode na janela recente de dois meses.", "model.noUsageTitle": "Sem uso", "model.noUsageDescription": "Nenhum uso chegou na janela atual.", "model.dailyTokenChart": "Gráfico diário de uso de tokens", - "model.usersDescription": "Usuários únicos diários do OpenCode Go na janela recente de dois meses.", + "model.usersDescription": "Usuários únicos diários do OpenCode na janela recente de dois meses.", "model.noUsersTitle": "Sem dados de usuários", "model.noUsersDescription": "Nenhuma linha com usuários chegou na janela atual.", "model.dailyUserChart": "Gráfico diário de usuários únicos", - "model.efficiencyDescription": "Custo, comportamento de cache e formato médio da sessão do OpenCode Go.", + "model.efficiencyDescription": "Custo, comportamento de cache e formato médio da sessão do OpenCode.", "model.noEfficiencyTitle": "Sem dados de eficiência", "model.noEfficiencyDescription": "Os dados de eficiência aparecem depois que o uso chega.", "model.totalSpendLabel": "Total spend", @@ -219,11 +216,9 @@ export const dict = { "model.tokensSession": "Tokens / sessão", "model.cacheRatio": "Taxa de cache", "model.inputTokens": "tokens de entrada", - "model.geoDescription": "Tokens do modelo OpenCode Go usados por país.", "model.noGeoTitle": "Sem dados geográficos", - "model.noGeoDescription": "Nenhuma linha geográfica do OpenCode Go correspondeu a este modelo.", - "model.worldMap": "Mapa-múndi do uso de tokens do modelo por país", - "model.peersDescription": "Modelos próximos por volume recente de tokens do OpenCode Go.", + "model.noGeoDescription": "Nenhuma linha geográfica do OpenCode correspondeu a este modelo.", + "model.peersDescription": "Modelos próximos por volume recente de tokens do OpenCode.", "model.noPeersTitle": "Sem pares", "model.noPeersDescription": "Os rankings de pares aparecem depois que o uso chega.", "model.noUsageLastWeek": "Sem uso na semana passada", diff --git a/packages/stats/app/src/i18n/da.ts b/packages/stats/app/src/i18n/da.ts index 35b4ad9972..5fdf2841e7 100644 --- a/packages/stats/app/src/i18n/da.ts +++ b/packages/stats/app/src/i18n/da.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "Ranglister for brug af AI-modeller | OpenCode Data", "app.description": - "Udforsk brugen af OpenCode Go på tværs af AI-modeller, inklusive tokenvolumen, ranglister, markedsandel, tokenpriser, sessionsomkostning, cacheandel og geografiske opdelinger.", + "Udforsk brugen af OpenCode på tværs af AI-modeller, inklusive tokenvolumen, ranglister, markedsandel, tokenpriser, sessionsomkostning, cacheandel og geografiske opdelinger.", "app.unfurlAlt": "OpenCode Data-ordmærke på en mørk mønstret baggrund", "nav.dataHome": "Dataforside", "nav.topModels": "Topmodeller", @@ -86,7 +86,7 @@ export const dict = { "home.loadingTitle": "Indlæser data", "home.loadingDescription": "Læser modelaggregater.", "home.usageTitle": "Brug", - "home.topModelsDescription": "Brug af modeller på tværs af OpenCode Go.", + "home.topModelsDescription": "Brug af modeller på tværs af OpenCode.", "home.noUsageTitle": "Ingen brugsdata", "home.noUsageDescription": "Ingen modelrækker matchede dette produkt og interval.", "home.noLeaderboardTitle": "Ingen ranglistedata", @@ -98,7 +98,7 @@ export const dict = { "home.modelUsers": "modelbrugere", "home.new": "Ny", "home.uniqueUsersTitle": "Unikke brugere", - "home.uniqueUsersDescription": "Daglige unikke OpenCode Go-brugere efter model.", + "home.uniqueUsersDescription": "Daglige unikke OpenCode-brugere efter model.", "home.noUserDataTitle": "Ingen brugerdata", "home.noUserDataDescription": "Ingen modelrækker med brugere matchede dette vindue.", "home.uniqueUsersChart": "Stablet diagram over unikke brugere efter model", @@ -109,11 +109,8 @@ export const dict = { "home.marketChart": "Markedsandel efter modelforfatter", "home.noData": "Ingen data", "home.geoTitle": "Geografisk opdeling", - "home.geoDescription": "Tokens brugt efter land.", "home.noGeoTitle": "Ingen geodata", "home.noGeoDescription": "Ingen georækker matchede dette interval.", - "home.worldMap": "Verdenskort over tokenbrug efter land", - "home.geoMapTitle": "Kort over geografisk opdeling", "home.unknown": "Ukendt", "home.tokenCostTitle": "Tokenomkostning", "home.tokenCostDescription": "Pris pr. 1 mio. tokens.", @@ -130,7 +127,7 @@ export const dict = { "home.costSession": "Omkostning/session", "lab.title": "{{lab}} AI-modelbrug og ranglister | OpenCode Data", "lab.description": - "Sammenlign {{lab}}-modeller brugt i OpenCode Go, inklusive tokenbrug, modelranglister, kontekstvinduer, udgivelsesdatoer, omkostninger og modelspecifikke data.", + "Sammenlign {{lab}}-modeller brugt i OpenCode, inklusive tokenbrug, modelranglister, kontekstvinduer, udgivelsesdatoer, omkostninger og modelspecifikke data.", "lab.loadingTitle": "Modellab", "lab.loadingDescription": "Læser modeltilgængelighed og seneste OpenCode-brug.", "lab.notFound": "Ingen modeller matchede dette lab.", @@ -156,7 +153,7 @@ export const dict = { "lab.share": "Andel", "model.title": "{{model}} brug, omkostning og rang | OpenCode Data", "model.description": - "Se OpenCode Go-brugsdata for {{model}}, inklusive tokenvolumen, ugentlig rang, tokenmix, omkostninger, cacheandel, sessioner, geografiske opdelinger og lignende modeller.", + "Se OpenCode-brugsdata for {{model}}, inklusive tokenvolumen, ugentlig rang, tokenmix, omkostninger, cacheandel, sessioner, geografiske opdelinger og lignende modeller.", "model.loadingTitle": "Modeldata", "model.loadingDescription": "Læser modelaggregater.", "model.loadingProfile": "Læser modelprofilen.", @@ -164,25 +161,24 @@ export const dict = { "model.noDataDescription": "Prøv at åbne en model fra ranglisten.", "model.noMatched": "Ingen modelfakta eller brugsrækker matchede {{id}}.", "model.fallback": "Model", - "model.catalogFallback": - "Modelfakta fra det fælles modelindeks. OpenCode Go-brug vises, når denne model har aktivitet.", - "model.unranked": "Ikke rangeret i sidste uges OpenCode Go-brug", - "model.ranked": "Rangeret #{{rank}} i sidste uges OpenCode Go-brug", + "model.catalogFallback": "Modelfakta fra det fælles modelindeks. OpenCode-brug vises, når denne model har aktivitet.", + "model.unranked": "Ikke rangeret i sidste uges OpenCode-brug", + "model.ranked": "Rangeret #{{rank}} i sidste uges OpenCode-brug", "model.observedVolume": "med {{share}} af observeret 2M-volumen.", "model.weights": "Modelvægte: {{label}}", "model.rank": "7D-rang", "model.profile": "Modelprofil", "model.listed": "Opført", - "model.noCurrentUsage": "Ingen OpenCode Go-brug i det aktuelle datavindue.", + "model.noCurrentUsage": "Ingen OpenCode-brug i det aktuelle datavindue.", "model.facts": "Modelfakta", "model.context": "Kontekst", "model.output": "Output", "model.knowledge": "Viden", "model.release": "Udgivelse", "model.inputs": "Input", - "model.overviewDescription": "Seneste OpenCode Go-tokens, unikke brugere og markedsposition.", + "model.overviewDescription": "Seneste OpenCode-tokens, unikke brugere og markedsposition.", "model.noSummaryTitle": "Ingen brugsoversigt", - "model.noSummaryDescription": "Denne model har endnu ingen OpenCode Go-brugsrækker.", + "model.noSummaryDescription": "Denne model har endnu ingen OpenCode-brugsrækker.", "model.tokens": "Tokens", "model.uniqueUsers": "Unikke brugere", "model.lastTwoMonths": "sidste to måneder", @@ -192,15 +188,15 @@ export const dict = { "model.totalModels": "{{count}} modeller", "model.momentum": "Momentum", "model.vsPreviousWindow": "vs forrige vindue", - "model.usageDescription": "Daglig OpenCode Go-tokenvolumen i det seneste tomånedersvindue.", + "model.usageDescription": "Daglig OpenCode-tokenvolumen i det seneste tomånedersvindue.", "model.noUsageTitle": "Ingen brug", "model.noUsageDescription": "Ingen brug landede i det aktuelle vindue.", "model.dailyTokenChart": "Dagligt diagram over tokenbrug", - "model.usersDescription": "Daglige unikke OpenCode Go-brugere i det seneste tomånedersvindue.", + "model.usersDescription": "Daglige unikke OpenCode-brugere i det seneste tomånedersvindue.", "model.noUsersTitle": "Ingen brugerdata", "model.noUsersDescription": "Ingen rækker med brugere landede i det aktuelle vindue.", "model.dailyUserChart": "Dagligt diagram over unikke brugere", - "model.efficiencyDescription": "Omkostning, cacheadfærd og gennemsnitlig OpenCode Go-sessionsform.", + "model.efficiencyDescription": "Omkostning, cacheadfærd og gennemsnitlig OpenCode-sessionsform.", "model.noEfficiencyTitle": "Ingen effektivitetsdata", "model.noEfficiencyDescription": "Effektivitetsdata vises, når brug lander.", "model.totalSpendLabel": "Total spend", @@ -218,11 +214,9 @@ export const dict = { "model.tokensSession": "Tokens / session", "model.cacheRatio": "Cacheandel", "model.inputTokens": "inputtokens", - "model.geoDescription": "OpenCode Go-modeltokens brugt efter land.", "model.noGeoTitle": "Ingen geodata", - "model.noGeoDescription": "Ingen OpenCode Go-georækker matchede denne model.", - "model.worldMap": "Verdenskort over modeltokenbrug efter land", - "model.peersDescription": "Nærliggende modeller efter seneste OpenCode Go-tokenvolumen.", + "model.noGeoDescription": "Ingen OpenCode-georækker matchede denne model.", + "model.peersDescription": "Nærliggende modeller efter seneste OpenCode-tokenvolumen.", "model.noPeersTitle": "Ingen lignende modeller", "model.noPeersDescription": "Ranglister over lignende modeller vises, når brug lander.", "model.noUsageLastWeek": "Ingen brug sidste uge", diff --git a/packages/stats/app/src/i18n/de.ts b/packages/stats/app/src/i18n/de.ts index 092d85c8c9..95a2f06fa7 100644 --- a/packages/stats/app/src/i18n/de.ts +++ b/packages/stats/app/src/i18n/de.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "KI-Modellnutzungs-Rankings | OpenCode Data", "app.description": - "Erkunde die OpenCode Go-Nutzung über KI-Modelle hinweg, einschließlich Tokenvolumen, Rankings, Marktanteil, Tokenpreisen, Sitzungskosten, Cache-Anteil und geografischen Aufschlüsselungen.", + "Erkunde die OpenCode-Nutzung über KI-Modelle hinweg, einschließlich Tokenvolumen, Rankings, Marktanteil, Tokenpreisen, Sitzungskosten, Cache-Anteil und geografischen Aufschlüsselungen.", "app.unfurlAlt": "OpenCode Data-Wortmarke auf dunklem gemustertem Hintergrund", "nav.dataHome": "Daten-Startseite", "nav.topModels": "Top-Modelle", @@ -86,7 +86,7 @@ export const dict = { "home.loadingTitle": "Daten werden geladen", "home.loadingDescription": "Modellaggregate werden gelesen.", "home.usageTitle": "Nutzung", - "home.topModelsDescription": "Nutzung von Modellen in OpenCode Go.", + "home.topModelsDescription": "Nutzung von Modellen in OpenCode.", "home.noUsageTitle": "Keine Nutzungsdaten", "home.noUsageDescription": "Keine Modellzeilen passten zu diesem Produkt und Zeitraum.", "home.noLeaderboardTitle": "Keine Bestenlistendaten", @@ -98,7 +98,7 @@ export const dict = { "home.modelUsers": "Modellnutzer", "home.new": "Neu", "home.uniqueUsersTitle": "Eindeutige Nutzer", - "home.uniqueUsersDescription": "Täglich eindeutige OpenCode Go-Nutzer nach Modell.", + "home.uniqueUsersDescription": "Täglich eindeutige OpenCode-Nutzer nach Modell.", "home.noUserDataTitle": "Keine Nutzerdaten", "home.noUserDataDescription": "Keine Modellzeilen mit Nutzern passten zu diesem Zeitraum.", "home.uniqueUsersChart": "Gestapeltes Diagramm eindeutiger Nutzer nach Modell", @@ -109,11 +109,8 @@ export const dict = { "home.marketChart": "Marktanteil nach Modellautor", "home.noData": "Keine Daten", "home.geoTitle": "Geografische Aufschlüsselung", - "home.geoDescription": "Nach Land verwendete Tokens.", "home.noGeoTitle": "Keine Geodaten", "home.noGeoDescription": "Keine Geozeilen passten zu diesem Zeitraum.", - "home.worldMap": "Weltkarte der Tokennutzung nach Land", - "home.geoMapTitle": "Karte der geografischen Aufschlüsselung", "home.unknown": "Unbekannt", "home.tokenCostTitle": "Tokenkosten", "home.tokenCostDescription": "Preis pro 1 Mio. Tokens.", @@ -130,7 +127,7 @@ export const dict = { "home.costSession": "Kosten/Sitzung", "lab.title": "{{lab}} KI-Modellnutzung und Rankings | OpenCode Data", "lab.description": - "Vergleiche {{lab}}-Modelle, die in OpenCode Go verwendet werden, einschließlich Tokennutzung, Modellrankings, Kontextfenstern, Veröffentlichungsdaten, Kosten und modellspezifischen Daten.", + "Vergleiche {{lab}}-Modelle, die in OpenCode verwendet werden, einschließlich Tokennutzung, Modellrankings, Kontextfenstern, Veröffentlichungsdaten, Kosten und modellspezifischen Daten.", "lab.loadingTitle": "Modelllabor", "lab.loadingDescription": "Modellverfügbarkeit und aktuelle OpenCode-Nutzung werden gelesen.", "lab.notFound": "Keine Modelle passten zu diesem Lab.", @@ -157,7 +154,7 @@ export const dict = { "lab.share": "Anteil", "model.title": "{{model}} Nutzung, Kosten und Rang | OpenCode Data", "model.description": - "Sieh dir OpenCode Go-Nutzungsdaten für {{model}} an, einschließlich Tokenvolumen, Wochenrang, Tokenmix, Kosten, Cache-Anteil, Sitzungen, geografischer Aufschlüsselung und Vergleichsmodellen.", + "Sieh dir OpenCode-Nutzungsdaten für {{model}} an, einschließlich Tokenvolumen, Wochenrang, Tokenmix, Kosten, Cache-Anteil, Sitzungen, geografischer Aufschlüsselung und Vergleichsmodellen.", "model.loadingTitle": "Modelldaten", "model.loadingDescription": "Modellaggregate werden gelesen.", "model.loadingProfile": "Modellprofil wird gelesen.", @@ -166,24 +163,24 @@ export const dict = { "model.noMatched": "Keine Modellfakten oder Nutzungszeilen passten zu {{id}}.", "model.fallback": "Modell", "model.catalogFallback": - "Modellfakten aus dem gemeinsamen Modellindex. OpenCode Go-Nutzung erscheint, sobald dieses Modell Aktivität hat.", - "model.unranked": "In der OpenCode Go-Nutzung der letzten Woche nicht platziert", - "model.ranked": "Rang #{{rank}} in der OpenCode Go-Nutzung der letzten Woche", + "Modellfakten aus dem gemeinsamen Modellindex. OpenCode-Nutzung erscheint, sobald dieses Modell Aktivität hat.", + "model.unranked": "In der OpenCode-Nutzung der letzten Woche nicht platziert", + "model.ranked": "Rang #{{rank}} in der OpenCode-Nutzung der letzten Woche", "model.observedVolume": "mit {{share}} des beobachteten 2M-Volumens.", "model.weights": "Modellgewichte: {{label}}", "model.rank": "7T-Rang", "model.profile": "Modellprofil", "model.listed": "Gelistet", - "model.noCurrentUsage": "Keine OpenCode Go-Nutzung im aktuellen Datenfenster.", + "model.noCurrentUsage": "Keine OpenCode-Nutzung im aktuellen Datenfenster.", "model.facts": "Modellfakten", "model.context": "Kontext", "model.output": "Ausgabe", "model.knowledge": "Wissensstand", "model.release": "Veröffentlichung", "model.inputs": "Eingaben", - "model.overviewDescription": "Aktuelle OpenCode Go-Tokens, eindeutige Nutzer und Marktposition.", + "model.overviewDescription": "Aktuelle OpenCode-Tokens, eindeutige Nutzer und Marktposition.", "model.noSummaryTitle": "Keine Nutzungsübersicht", - "model.noSummaryDescription": "Dieses Modell hat noch keine OpenCode Go-Nutzungszeilen.", + "model.noSummaryDescription": "Dieses Modell hat noch keine OpenCode-Nutzungszeilen.", "model.tokens": "Tokens", "model.uniqueUsers": "Eindeutige Nutzer", "model.lastTwoMonths": "letzte zwei Monate", @@ -193,15 +190,15 @@ export const dict = { "model.totalModels": "{{count}} Modelle", "model.momentum": "Momentum", "model.vsPreviousWindow": "ggü. vorherigem Fenster", - "model.usageDescription": "Tägliches OpenCode Go-Tokenvolumen im aktuellen Zwei-Monats-Fenster.", + "model.usageDescription": "Tägliches OpenCode-Tokenvolumen im aktuellen Zwei-Monats-Fenster.", "model.noUsageTitle": "Keine Nutzung", "model.noUsageDescription": "Im aktuellen Fenster ist keine Nutzung eingegangen.", "model.dailyTokenChart": "Diagramm der täglichen Tokennutzung", - "model.usersDescription": "Täglich eindeutige OpenCode Go-Nutzer im aktuellen Zwei-Monats-Fenster.", + "model.usersDescription": "Täglich eindeutige OpenCode-Nutzer im aktuellen Zwei-Monats-Fenster.", "model.noUsersTitle": "Keine Nutzerdaten", "model.noUsersDescription": "Im aktuellen Fenster sind keine Zeilen mit Nutzern eingegangen.", "model.dailyUserChart": "Diagramm der täglich eindeutigen Nutzer", - "model.efficiencyDescription": "Kosten, Cache-Verhalten und durchschnittliche OpenCode Go-Sitzungsform.", + "model.efficiencyDescription": "Kosten, Cache-Verhalten und durchschnittliche OpenCode-Sitzungsform.", "model.noEfficiencyTitle": "Keine Effizienzdaten", "model.noEfficiencyDescription": "Effizienzdaten erscheinen, nachdem Nutzung eingegangen ist.", "model.totalSpendLabel": "Total spend", @@ -219,11 +216,9 @@ export const dict = { "model.tokensSession": "Tokens / Sitzung", "model.cacheRatio": "Cache-Anteil", "model.inputTokens": "Eingabetokens", - "model.geoDescription": "OpenCode Go-Modelltokens nach Land.", "model.noGeoTitle": "Keine Geodaten", - "model.noGeoDescription": "Keine OpenCode Go-Geozeilen passten zu diesem Modell.", - "model.worldMap": "Weltkarte der Modelltokennutzung nach Land", - "model.peersDescription": "Nahe Modelle nach aktuellem OpenCode Go-Tokenvolumen.", + "model.noGeoDescription": "Keine OpenCode-Geozeilen passten zu diesem Modell.", + "model.peersDescription": "Nahe Modelle nach aktuellem OpenCode-Tokenvolumen.", "model.noPeersTitle": "Keine Vergleichsmodelle", "model.noPeersDescription": "Vergleichsrankings erscheinen, nachdem Nutzung eingegangen ist.", "model.noUsageLastWeek": "Keine Nutzung letzte Woche", diff --git a/packages/stats/app/src/i18n/es.ts b/packages/stats/app/src/i18n/es.ts index a326d913f0..78d2e61c24 100644 --- a/packages/stats/app/src/i18n/es.ts +++ b/packages/stats/app/src/i18n/es.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "Clasificaciones de uso de modelos de IA | OpenCode Data", "app.description": - "Explora el uso de OpenCode Go en modelos de IA, incluido el volumen de tokens, clasificaciones, cuota de mercado, precios de tokens, coste por sesión, ratio de caché y desgloses geográficos.", + "Explora el uso de OpenCode en modelos de IA, incluido el volumen de tokens, clasificaciones, cuota de mercado, precios de tokens, coste por sesión, ratio de caché y desgloses geográficos.", "app.unfurlAlt": "Logotipo textual de OpenCode Data sobre un fondo oscuro con patrón", "nav.dataHome": "Inicio de datos", "nav.topModels": "Modelos principales", @@ -85,7 +85,7 @@ export const dict = { "home.loadingTitle": "Cargando datos", "home.loadingDescription": "Leyendo agregados de modelos.", "home.usageTitle": "Uso", - "home.topModelsDescription": "Uso de modelos en OpenCode Go.", + "home.topModelsDescription": "Uso de modelos en OpenCode.", "home.noUsageTitle": "Sin datos de uso", "home.noUsageDescription": "Ninguna fila de modelo coincidió con este producto y rango.", "home.noLeaderboardTitle": "Sin datos de clasificación", @@ -97,7 +97,7 @@ export const dict = { "home.modelUsers": "usuarios del modelo", "home.new": "Nuevo", "home.uniqueUsersTitle": "Usuarios únicos", - "home.uniqueUsersDescription": "Usuarios únicos diarios de OpenCode Go por modelo.", + "home.uniqueUsersDescription": "Usuarios únicos diarios de OpenCode por modelo.", "home.noUserDataTitle": "Sin datos de usuarios", "home.noUserDataDescription": "Ninguna fila de modelo con usuarios coincidió con esta ventana.", "home.uniqueUsersChart": "Gráfico apilado de usuarios únicos por modelo", @@ -108,11 +108,8 @@ export const dict = { "home.marketChart": "Cuota de mercado por autor del modelo", "home.noData": "Sin datos", "home.geoTitle": "Desglose geográfico", - "home.geoDescription": "Tokens usados por país.", "home.noGeoTitle": "Sin datos geográficos", "home.noGeoDescription": "Ninguna fila geográfica coincidió con este rango.", - "home.worldMap": "Mapa mundial del uso de tokens por país", - "home.geoMapTitle": "Mapa de desglose geográfico", "home.unknown": "Desconocido", "home.tokenCostTitle": "Coste de tokens", "home.tokenCostDescription": "Precio por 1 M de tokens.", @@ -129,7 +126,7 @@ export const dict = { "home.costSession": "Coste/sesión", "lab.title": "Uso y clasificaciones de modelos de IA de {{lab}} | OpenCode Data", "lab.description": - "Compara modelos de {{lab}} usados en OpenCode Go, incluido uso de tokens, clasificaciones de modelos, ventanas de contexto, fechas de lanzamiento, costes y datos específicos del modelo.", + "Compara modelos de {{lab}} usados en OpenCode, incluido uso de tokens, clasificaciones de modelos, ventanas de contexto, fechas de lanzamiento, costes y datos específicos del modelo.", "lab.loadingTitle": "Laboratorio de modelos", "lab.loadingDescription": "Leyendo disponibilidad de modelos y uso reciente de OpenCode.", "lab.notFound": "Ningún modelo coincidió con este laboratorio.", @@ -156,7 +153,7 @@ export const dict = { "lab.share": "Cuota", "model.title": "Uso, coste y posición de {{model}} | OpenCode Data", "model.description": - "Consulta datos de uso de {{model}} en OpenCode Go, incluido volumen de tokens, posición semanal, mezcla de tokens, costes, ratio de caché, sesiones, desgloses geográficos y modelos similares.", + "Consulta datos de uso de {{model}} en OpenCode, incluido volumen de tokens, posición semanal, mezcla de tokens, costes, ratio de caché, sesiones, desgloses geográficos y modelos similares.", "model.loadingTitle": "Datos del modelo", "model.loadingDescription": "Leyendo agregados del modelo.", "model.loadingProfile": "Leyendo el perfil del modelo.", @@ -165,24 +162,24 @@ export const dict = { "model.noMatched": "Ningún dato del modelo ni fila de uso coincidió con {{id}}.", "model.fallback": "Modelo", "model.catalogFallback": - "Datos del modelo desde el índice compartido de modelos. El uso de OpenCode Go aparecerá cuando este modelo tenga actividad.", - "model.unranked": "Sin clasificación en el uso de OpenCode Go de la semana pasada", - "model.ranked": "Clasificado #{{rank}} en el uso de OpenCode Go de la semana pasada", + "Datos del modelo desde el índice compartido de modelos. El uso de OpenCode aparecerá cuando este modelo tenga actividad.", + "model.unranked": "Sin clasificación en el uso de OpenCode de la semana pasada", + "model.ranked": "Clasificado #{{rank}} en el uso de OpenCode de la semana pasada", "model.observedVolume": "con {{share}} del volumen 2M observado.", "model.weights": "Pesos del modelo: {{label}}", "model.rank": "Posición 7D", "model.profile": "Perfil del modelo", "model.listed": "Listado", - "model.noCurrentUsage": "Sin uso de OpenCode Go en la ventana de datos actual.", + "model.noCurrentUsage": "Sin uso de OpenCode en la ventana de datos actual.", "model.facts": "Datos del modelo", "model.context": "Contexto", "model.output": "Salida", "model.knowledge": "Conocimiento", "model.release": "Lanzamiento", "model.inputs": "Entradas", - "model.overviewDescription": "Tokens recientes de OpenCode Go, usuarios únicos y posición de mercado.", + "model.overviewDescription": "Tokens recientes de OpenCode, usuarios únicos y posición de mercado.", "model.noSummaryTitle": "Sin resumen de uso", - "model.noSummaryDescription": "Este modelo aún no tiene filas de uso de OpenCode Go.", + "model.noSummaryDescription": "Este modelo aún no tiene filas de uso de OpenCode.", "model.tokens": "Tokens", "model.uniqueUsers": "Usuarios únicos", "model.lastTwoMonths": "últimos dos meses", @@ -192,15 +189,15 @@ export const dict = { "model.totalModels": "{{count}} modelos", "model.momentum": "Impulso", "model.vsPreviousWindow": "vs ventana anterior", - "model.usageDescription": "Volumen diario de tokens de OpenCode Go durante la ventana reciente de dos meses.", + "model.usageDescription": "Volumen diario de tokens de OpenCode durante la ventana reciente de dos meses.", "model.noUsageTitle": "Sin uso", "model.noUsageDescription": "No llegó uso en la ventana actual.", "model.dailyTokenChart": "Gráfico de uso diario de tokens", - "model.usersDescription": "Usuarios únicos diarios de OpenCode Go durante la ventana reciente de dos meses.", + "model.usersDescription": "Usuarios únicos diarios de OpenCode durante la ventana reciente de dos meses.", "model.noUsersTitle": "Sin datos de usuarios", "model.noUsersDescription": "No llegaron filas con usuarios en la ventana actual.", "model.dailyUserChart": "Gráfico diario de usuarios únicos", - "model.efficiencyDescription": "Coste, comportamiento de caché y forma media de sesión de OpenCode Go.", + "model.efficiencyDescription": "Coste, comportamiento de caché y forma media de sesión de OpenCode.", "model.noEfficiencyTitle": "Sin datos de eficiencia", "model.noEfficiencyDescription": "Los datos de eficiencia aparecen después de que llegue uso.", "model.totalSpendLabel": "Total spend", @@ -218,11 +215,9 @@ export const dict = { "model.tokensSession": "Tokens / sesión", "model.cacheRatio": "Ratio de caché", "model.inputTokens": "tokens de entrada", - "model.geoDescription": "Tokens del modelo de OpenCode Go usados por país.", "model.noGeoTitle": "Sin datos geográficos", - "model.noGeoDescription": "Ninguna fila geográfica de OpenCode Go coincidió con este modelo.", - "model.worldMap": "Mapa mundial del uso de tokens del modelo por país", - "model.peersDescription": "Modelos cercanos por volumen reciente de tokens de OpenCode Go.", + "model.noGeoDescription": "Ninguna fila geográfica de OpenCode coincidió con este modelo.", + "model.peersDescription": "Modelos cercanos por volumen reciente de tokens de OpenCode.", "model.noPeersTitle": "Sin modelos similares", "model.noPeersDescription": "Las clasificaciones de modelos similares aparecen después de que llegue uso.", "model.noUsageLastWeek": "Sin uso la semana pasada", diff --git a/packages/stats/app/src/i18n/fr.ts b/packages/stats/app/src/i18n/fr.ts index fc0c103495..bbc1d5cca7 100644 --- a/packages/stats/app/src/i18n/fr.ts +++ b/packages/stats/app/src/i18n/fr.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "Classements d'utilisation des modèles d'IA | OpenCode Data", "app.description": - "Explorez l'utilisation d'OpenCode Go par modèle d'IA, avec le volume de tokens, les classements, la part de marché, le prix des tokens, le coût par session, le taux de cache et les répartitions géographiques.", + "Explorez l'utilisation d'OpenCode par modèle d'IA, avec le volume de tokens, les classements, la part de marché, le prix des tokens, le coût par session, le taux de cache et les répartitions géographiques.", "app.unfurlAlt": "Logo textuel OpenCode Data sur un fond sombre à motif", "nav.dataHome": "Accueil des données", "nav.topModels": "Meilleurs modèles", @@ -86,7 +86,7 @@ export const dict = { "home.loadingTitle": "Chargement des données", "home.loadingDescription": "Lecture des agrégats de modèles.", "home.usageTitle": "Utilisation", - "home.topModelsDescription": "Utilisation des modèles dans OpenCode Go.", + "home.topModelsDescription": "Utilisation des modèles dans OpenCode.", "home.noUsageTitle": "Aucune donnée d'utilisation", "home.noUsageDescription": "Aucune ligne de modèle ne correspondait à ce produit et à cette période.", "home.noLeaderboardTitle": "Aucune donnée de classement", @@ -98,7 +98,7 @@ export const dict = { "home.modelUsers": "utilisateurs du modèle", "home.new": "Nouveau", "home.uniqueUsersTitle": "Utilisateurs uniques", - "home.uniqueUsersDescription": "Utilisateurs uniques quotidiens d'OpenCode Go par modèle.", + "home.uniqueUsersDescription": "Utilisateurs uniques quotidiens d'OpenCode par modèle.", "home.noUserDataTitle": "Aucune donnée utilisateur", "home.noUserDataDescription": "Aucune ligne de modèle avec utilisateurs ne correspondait à cette fenêtre.", "home.uniqueUsersChart": "Graphique empilé des utilisateurs uniques par modèle", @@ -109,11 +109,8 @@ export const dict = { "home.marketChart": "Part de marché par auteur de modèle", "home.noData": "Aucune donnée", "home.geoTitle": "Répartition géographique", - "home.geoDescription": "Tokens utilisés par pays.", "home.noGeoTitle": "Aucune donnée géographique", "home.noGeoDescription": "Aucune ligne géographique ne correspondait à cette période.", - "home.worldMap": "Carte mondiale de l'utilisation des tokens par pays", - "home.geoMapTitle": "Carte de répartition géographique", "home.unknown": "Inconnu", "home.tokenCostTitle": "Coût des tokens", "home.tokenCostDescription": "Prix par million de tokens.", @@ -130,7 +127,7 @@ export const dict = { "home.costSession": "Coût/session", "lab.title": "Utilisation et classements des modèles d'IA {{lab}} | OpenCode Data", "lab.description": - "Comparez les modèles {{lab}} utilisés dans OpenCode Go, avec l'utilisation des tokens, les classements de modèles, les fenêtres de contexte, les dates de sortie, les coûts et les données propres aux modèles.", + "Comparez les modèles {{lab}} utilisés dans OpenCode, avec l'utilisation des tokens, les classements de modèles, les fenêtres de contexte, les dates de sortie, les coûts et les données propres aux modèles.", "lab.loadingTitle": "Laboratoire de modèles", "lab.loadingDescription": "Lecture de la disponibilité des modèles et de l'utilisation récente d'OpenCode.", "lab.notFound": "Aucun modèle ne correspondait à ce laboratoire.", @@ -158,7 +155,7 @@ export const dict = { "lab.share": "Part", "model.title": "Utilisation, coût et rang de {{model}} | OpenCode Data", "model.description": - "Consultez les données d'utilisation OpenCode Go de {{model}}, avec le volume de tokens, le rang hebdomadaire, la répartition des tokens, les coûts, le taux de cache, les sessions, les répartitions géographiques et les modèles proches.", + "Consultez les données d'utilisation OpenCode de {{model}}, avec le volume de tokens, le rang hebdomadaire, la répartition des tokens, les coûts, le taux de cache, les sessions, les répartitions géographiques et les modèles proches.", "model.loadingTitle": "Données du modèle", "model.loadingDescription": "Lecture des agrégats du modèle.", "model.loadingProfile": "Lecture du profil du modèle.", @@ -167,24 +164,24 @@ export const dict = { "model.noMatched": "Aucun fait de modèle ni aucune ligne d'utilisation ne correspondait à {{id}}.", "model.fallback": "Modèle", "model.catalogFallback": - "Faits du modèle issus de l'index partagé des modèles. L'utilisation OpenCode Go apparaîtra dès que ce modèle aura de l'activité.", - "model.unranked": "Non classé dans l'utilisation OpenCode Go de la semaine dernière", - "model.ranked": "Classé #{{rank}} dans l'utilisation OpenCode Go de la semaine dernière", + "Faits du modèle issus de l'index partagé des modèles. L'utilisation OpenCode apparaîtra dès que ce modèle aura de l'activité.", + "model.unranked": "Non classé dans l'utilisation OpenCode de la semaine dernière", + "model.ranked": "Classé #{{rank}} dans l'utilisation OpenCode de la semaine dernière", "model.observedVolume": "avec {{share}} du volume 2M observé.", "model.weights": "Poids du modèle : {{label}}", "model.rank": "Rang 7 j", "model.profile": "Profil du modèle", "model.listed": "Répertorié", - "model.noCurrentUsage": "Aucune utilisation OpenCode Go dans la fenêtre de données actuelle.", + "model.noCurrentUsage": "Aucune utilisation OpenCode dans la fenêtre de données actuelle.", "model.facts": "Faits du modèle", "model.context": "Contexte", "model.output": "Sortie", "model.knowledge": "Connaissances", "model.release": "Sortie", "model.inputs": "Entrées", - "model.overviewDescription": "Tokens OpenCode Go récents, utilisateurs uniques et position sur le marché.", + "model.overviewDescription": "Tokens OpenCode récents, utilisateurs uniques et position sur le marché.", "model.noSummaryTitle": "Aucun résumé d'utilisation", - "model.noSummaryDescription": "Ce modèle n'a pas encore de lignes d'utilisation OpenCode Go.", + "model.noSummaryDescription": "Ce modèle n'a pas encore de lignes d'utilisation OpenCode.", "model.tokens": "Tokens", "model.uniqueUsers": "Utilisateurs uniques", "model.lastTwoMonths": "deux derniers mois", @@ -194,15 +191,15 @@ export const dict = { "model.totalModels": "{{count}} modèles", "model.momentum": "Dynamique", "model.vsPreviousWindow": "vs fenêtre précédente", - "model.usageDescription": "Volume quotidien de tokens OpenCode Go sur la fenêtre récente de deux mois.", + "model.usageDescription": "Volume quotidien de tokens OpenCode sur la fenêtre récente de deux mois.", "model.noUsageTitle": "Aucune utilisation", "model.noUsageDescription": "Aucune utilisation n'est arrivée dans la fenêtre actuelle.", "model.dailyTokenChart": "Graphique quotidien d'utilisation des tokens", - "model.usersDescription": "Utilisateurs uniques quotidiens d'OpenCode Go sur la fenêtre récente de deux mois.", + "model.usersDescription": "Utilisateurs uniques quotidiens d'OpenCode sur la fenêtre récente de deux mois.", "model.noUsersTitle": "Aucune donnée utilisateur", "model.noUsersDescription": "Aucune ligne avec utilisateurs n'est arrivée dans la fenêtre actuelle.", "model.dailyUserChart": "Graphique quotidien des utilisateurs uniques", - "model.efficiencyDescription": "Coût, comportement du cache et forme moyenne des sessions OpenCode Go.", + "model.efficiencyDescription": "Coût, comportement du cache et forme moyenne des sessions OpenCode.", "model.noEfficiencyTitle": "Aucune donnée d'efficacité", "model.noEfficiencyDescription": "Les données d'efficacité apparaissent après l'arrivée de l'utilisation.", "model.totalSpendLabel": "Total spend", @@ -220,11 +217,9 @@ export const dict = { "model.tokensSession": "Tokens / session", "model.cacheRatio": "Taux de cache", "model.inputTokens": "tokens d'entrée", - "model.geoDescription": "Tokens du modèle OpenCode Go utilisés par pays.", "model.noGeoTitle": "Aucune donnée géographique", - "model.noGeoDescription": "Aucune ligne géographique OpenCode Go ne correspondait à ce modèle.", - "model.worldMap": "Carte mondiale de l'utilisation des tokens du modèle par pays", - "model.peersDescription": "Modèles proches par volume récent de tokens OpenCode Go.", + "model.noGeoDescription": "Aucune ligne géographique OpenCode ne correspondait à ce modèle.", + "model.peersDescription": "Modèles proches par volume récent de tokens OpenCode.", "model.noPeersTitle": "Aucun modèle proche", "model.noPeersDescription": "Les classements de modèles proches apparaissent après l'arrivée de l'utilisation.", "model.noUsageLastWeek": "Aucune utilisation la semaine dernière", diff --git a/packages/stats/app/src/i18n/it.ts b/packages/stats/app/src/i18n/it.ts index 83c39f17bb..e4a4b12969 100644 --- a/packages/stats/app/src/i18n/it.ts +++ b/packages/stats/app/src/i18n/it.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "Classifiche di utilizzo dei modelli IA | OpenCode Data", "app.description": - "Esplora l'utilizzo di OpenCode Go nei modelli IA, inclusi volume di token, classifiche, quota di mercato, prezzi dei token, costo per sessione, rapporto cache e ripartizioni geografiche.", + "Esplora l'utilizzo di OpenCode nei modelli IA, inclusi volume di token, classifiche, quota di mercato, prezzi dei token, costo per sessione, rapporto cache e ripartizioni geografiche.", "app.unfurlAlt": "Logotipo OpenCode Data su uno sfondo scuro con motivo", "nav.dataHome": "Home dati", "nav.topModels": "Modelli principali", @@ -86,7 +86,7 @@ export const dict = { "home.loadingTitle": "Caricamento dati", "home.loadingDescription": "Lettura degli aggregati dei modelli.", "home.usageTitle": "Utilizzo", - "home.topModelsDescription": "Utilizzo dei modelli in OpenCode Go.", + "home.topModelsDescription": "Utilizzo dei modelli in OpenCode.", "home.noUsageTitle": "Nessun dato di utilizzo", "home.noUsageDescription": "Nessuna riga modello corrispondeva a questo prodotto e intervallo.", "home.noLeaderboardTitle": "Nessun dato di classifica", @@ -98,7 +98,7 @@ export const dict = { "home.modelUsers": "utenti del modello", "home.new": "Nuovo", "home.uniqueUsersTitle": "Utenti unici", - "home.uniqueUsersDescription": "Utenti unici giornalieri di OpenCode Go per modello.", + "home.uniqueUsersDescription": "Utenti unici giornalieri di OpenCode per modello.", "home.noUserDataTitle": "Nessun dato utente", "home.noUserDataDescription": "Nessuna riga modello con utenti corrispondeva a questa finestra.", "home.uniqueUsersChart": "Grafico impilato degli utenti unici per modello", @@ -109,11 +109,8 @@ export const dict = { "home.marketChart": "Quota di mercato per autore del modello", "home.noData": "Nessun dato", "home.geoTitle": "Ripartizione geografica", - "home.geoDescription": "Token usati per paese.", "home.noGeoTitle": "Nessun dato geografico", "home.noGeoDescription": "Nessuna riga geografica corrispondeva a questo intervallo.", - "home.worldMap": "Mappa mondiale dell'utilizzo dei token per paese", - "home.geoMapTitle": "Mappa della ripartizione geografica", "home.unknown": "Sconosciuto", "home.tokenCostTitle": "Costo token", "home.tokenCostDescription": "Prezzo per 1 M di token.", @@ -130,7 +127,7 @@ export const dict = { "home.costSession": "Costo/sessione", "lab.title": "Utilizzo e classifiche dei modelli IA {{lab}} | OpenCode Data", "lab.description": - "Confronta i modelli {{lab}} usati in OpenCode Go, inclusi utilizzo dei token, classifiche dei modelli, finestre di contesto, date di rilascio, costi e dati specifici del modello.", + "Confronta i modelli {{lab}} usati in OpenCode, inclusi utilizzo dei token, classifiche dei modelli, finestre di contesto, date di rilascio, costi e dati specifici del modello.", "lab.loadingTitle": "Laboratorio modelli", "lab.loadingDescription": "Lettura della disponibilità dei modelli e dell'utilizzo recente di OpenCode.", "lab.notFound": "Nessun modello corrispondeva a questo laboratorio.", @@ -157,7 +154,7 @@ export const dict = { "lab.share": "Quota", "model.title": "Utilizzo, costo e posizione di {{model}} | OpenCode Data", "model.description": - "Visualizza i dati di utilizzo di {{model}} in OpenCode Go, inclusi volume di token, rango settimanale, mix di token, costi, rapporto cache, sessioni, ripartizioni geografiche e modelli simili.", + "Visualizza i dati di utilizzo di {{model}} in OpenCode, inclusi volume di token, rango settimanale, mix di token, costi, rapporto cache, sessioni, ripartizioni geografiche e modelli simili.", "model.loadingTitle": "Dati modello", "model.loadingDescription": "Lettura degli aggregati del modello.", "model.loadingProfile": "Lettura del profilo del modello.", @@ -166,24 +163,24 @@ export const dict = { "model.noMatched": "Nessun fatto modello o riga di utilizzo corrispondeva a {{id}}.", "model.fallback": "Modello", "model.catalogFallback": - "Fatti del modello dall'indice condiviso dei modelli. L'utilizzo di OpenCode Go appare quando questo modello ha attività.", - "model.unranked": "Non classificato nell'utilizzo OpenCode Go della scorsa settimana", - "model.ranked": "Posizione #{{rank}} nell'utilizzo OpenCode Go della scorsa settimana", + "Fatti del modello dall'indice condiviso dei modelli. L'utilizzo di OpenCode appare quando questo modello ha attività.", + "model.unranked": "Non classificato nell'utilizzo OpenCode della scorsa settimana", + "model.ranked": "Posizione #{{rank}} nell'utilizzo OpenCode della scorsa settimana", "model.observedVolume": "con {{share}} del volume 2M osservato.", "model.weights": "Pesi del modello: {{label}}", "model.rank": "Posizione 7G", "model.profile": "Profilo modello", "model.listed": "Elencato", - "model.noCurrentUsage": "Nessun utilizzo di OpenCode Go nella finestra dati attuale.", + "model.noCurrentUsage": "Nessun utilizzo di OpenCode nella finestra dati attuale.", "model.facts": "Fatti del modello", "model.context": "Contesto", "model.output": "Output", "model.knowledge": "Conoscenza", "model.release": "Versione", "model.inputs": "Input", - "model.overviewDescription": "Token recenti di OpenCode Go, utenti unici e posizione di mercato.", + "model.overviewDescription": "Token recenti di OpenCode, utenti unici e posizione di mercato.", "model.noSummaryTitle": "Nessun riepilogo di utilizzo", - "model.noSummaryDescription": "Questo modello non ha ancora righe di utilizzo di OpenCode Go.", + "model.noSummaryDescription": "Questo modello non ha ancora righe di utilizzo di OpenCode.", "model.tokens": "Token", "model.uniqueUsers": "Utenti unici", "model.lastTwoMonths": "ultimi due mesi", @@ -193,15 +190,15 @@ export const dict = { "model.totalModels": "{{count}} modelli", "model.momentum": "Slancio", "model.vsPreviousWindow": "vs finestra precedente", - "model.usageDescription": "Volume giornaliero di token OpenCode Go nella recente finestra di due mesi.", + "model.usageDescription": "Volume giornaliero di token OpenCode nella recente finestra di due mesi.", "model.noUsageTitle": "Nessun utilizzo", "model.noUsageDescription": "Nessun utilizzo è arrivato nella finestra attuale.", "model.dailyTokenChart": "Grafico giornaliero di utilizzo dei token", - "model.usersDescription": "Utenti unici giornalieri di OpenCode Go nella recente finestra di due mesi.", + "model.usersDescription": "Utenti unici giornalieri di OpenCode nella recente finestra di due mesi.", "model.noUsersTitle": "Nessun dato utente", "model.noUsersDescription": "Nessuna riga con utenti è arrivata nella finestra attuale.", "model.dailyUserChart": "Grafico giornaliero degli utenti unici", - "model.efficiencyDescription": "Costi, comportamento della cache e forma media della sessione OpenCode Go.", + "model.efficiencyDescription": "Costi, comportamento della cache e forma media della sessione OpenCode.", "model.noEfficiencyTitle": "Nessun dato di efficienza", "model.noEfficiencyDescription": "I dati di efficienza appaiono dopo l'arrivo dell'utilizzo.", "model.totalSpendLabel": "Total spend", @@ -219,11 +216,9 @@ export const dict = { "model.tokensSession": "Token / sessione", "model.cacheRatio": "Rapporto cache", "model.inputTokens": "token di input", - "model.geoDescription": "Token del modello OpenCode Go usati per paese.", "model.noGeoTitle": "Nessun dato geografico", - "model.noGeoDescription": "Nessuna riga geografica OpenCode Go corrispondeva a questo modello.", - "model.worldMap": "Mappa mondiale dell'utilizzo dei token del modello per paese", - "model.peersDescription": "Modelli vicini per volume recente di token OpenCode Go.", + "model.noGeoDescription": "Nessuna riga geografica OpenCode corrispondeva a questo modello.", + "model.peersDescription": "Modelli vicini per volume recente di token OpenCode.", "model.noPeersTitle": "Nessun modello simile", "model.noPeersDescription": "Le classifiche dei modelli simili appaiono dopo l'arrivo dell'utilizzo.", "model.noUsageLastWeek": "Nessun utilizzo la scorsa settimana", diff --git a/packages/stats/app/src/i18n/ja.ts b/packages/stats/app/src/i18n/ja.ts index ddd577cfc3..1826329523 100644 --- a/packages/stats/app/src/i18n/ja.ts +++ b/packages/stats/app/src/i18n/ja.ts @@ -3,7 +3,7 @@ import type { Key } from "../i18n" export const dict = { "app.title": "AIモデル使用ランキング | OpenCodeデータ", "app.description": - "OpenCode GoでのAIモデル利用状況を、トークン量、ランキング、マーケットシェア、トークン価格、セッションコスト、キャッシュ比率、地域別内訳まで確認できます。", + "OpenCodeでのAIモデル利用状況を、トークン量、ランキング、マーケットシェア、トークン価格、セッションコスト、キャッシュ比率、地域別内訳まで確認できます。", "app.unfurlAlt": "暗いパターン背景上のOpenCode Dataワードマーク", "nav.dataHome": "データホーム", "nav.topModels": "上位モデル", @@ -88,7 +88,7 @@ export const dict = { "home.loadingTitle": "データを読み込み中", "home.loadingDescription": "モデル集計を読み込んでいます。", "home.usageTitle": "使用量", - "home.topModelsDescription": "OpenCode Goにおけるモデルの使用状況。", + "home.topModelsDescription": "OpenCodeにおけるモデルの使用状況。", "home.noUsageTitle": "使用データがありません", "home.noUsageDescription": "このプロダクトと期間に一致するモデル行はありません。", "home.noLeaderboardTitle": "ランキングデータがありません", @@ -100,7 +100,7 @@ export const dict = { "home.modelUsers": "モデルユーザー", "home.new": "新規", "home.uniqueUsersTitle": "ユニークユーザー", - "home.uniqueUsersDescription": "モデル別のOpenCode Go日次ユニークユーザー。", + "home.uniqueUsersDescription": "モデル別のOpenCode日次ユニークユーザー。", "home.noUserDataTitle": "ユーザーデータがありません", "home.noUserDataDescription": "この期間に一致するユーザー付きモデル行はありません。", "home.uniqueUsersChart": "モデル別ユニークユーザーの積み上げグラフ", @@ -111,11 +111,8 @@ export const dict = { "home.marketChart": "モデル作者別マーケットシェア", "home.noData": "データなし", "home.geoTitle": "地域別内訳", - "home.geoDescription": "国別のトークン使用量。", "home.noGeoTitle": "地域データがありません", "home.noGeoDescription": "この期間に一致する地域行はありません。", - "home.worldMap": "国別トークン使用量の世界地図", - "home.geoMapTitle": "地域別内訳マップ", "home.unknown": "不明", "home.tokenCostTitle": "トークンコスト", "home.tokenCostDescription": "100万トークンあたりの価格。", @@ -132,7 +129,7 @@ export const dict = { "home.costSession": "コスト/セッション", "lab.title": "{{lab}} AIモデル使用量とランキング | OpenCodeデータ", "lab.description": - "OpenCode Goで使用されている{{lab}}モデルを、トークン使用量、モデルランキング、コンテキストウィンドウ、リリース日、コスト、モデル別データで比較できます。", + "OpenCodeで使用されている{{lab}}モデルを、トークン使用量、モデルランキング、コンテキストウィンドウ、リリース日、コスト、モデル別データで比較できます。", "lab.loadingTitle": "モデルラボ", "lab.loadingDescription": "モデルの利用可否と最近のOpenCode使用状況を読み込んでいます。", "lab.notFound": "このラボに一致するモデルはありません。", @@ -158,7 +155,7 @@ export const dict = { "lab.share": "シェア", "model.title": "{{model}}の使用量、コスト、ランク | OpenCodeデータ", "model.description": - "{{model}}のOpenCode Go使用データを、トークン量、週間ランク、トークン構成、コスト、キャッシュ比率、セッション、地域別内訳、類似モデルまで確認できます。", + "{{model}}のOpenCode使用データを、トークン量、週間ランク、トークン構成、コスト、キャッシュ比率、セッション、地域別内訳、類似モデルまで確認できます。", "model.loadingTitle": "モデルデータ", "model.loadingDescription": "モデル集計を読み込んでいます。", "model.loadingProfile": "モデルプロフィールを読み込んでいます。", @@ -167,24 +164,24 @@ export const dict = { "model.noMatched": "{{id}}に一致するモデル情報または使用行はありません。", "model.fallback": "モデル", "model.catalogFallback": - "共有モデルインデックスのモデル情報です。このモデルにアクティビティが発生するとOpenCode Go使用量が表示されます。", - "model.unranked": "先週のOpenCode Go使用量では未ランク", - "model.ranked": "先週のOpenCode Go使用量で#{{rank}}位", + "共有モデルインデックスのモデル情報です。このモデルにアクティビティが発生するとOpenCode使用量が表示されます。", + "model.unranked": "先週のOpenCode使用量では未ランク", + "model.ranked": "先週のOpenCode使用量で#{{rank}}位", "model.observedVolume": "観測された2M量の{{share}}。", "model.weights": "モデル重み: {{label}}", "model.rank": "7日ランク", "model.profile": "モデルプロフィール", "model.listed": "掲載日", - "model.noCurrentUsage": "現在のデータ期間にはOpenCode Go使用量がありません。", + "model.noCurrentUsage": "現在のデータ期間にはOpenCode使用量がありません。", "model.facts": "モデル情報", "model.context": "コンテキスト", "model.output": "出力", "model.knowledge": "知識", "model.release": "リリース", "model.inputs": "入力", - "model.overviewDescription": "最近のOpenCode Goトークン、ユニークユーザー、市場での位置。", + "model.overviewDescription": "最近のOpenCodeトークン、ユニークユーザー、市場での位置。", "model.noSummaryTitle": "使用概要がありません", - "model.noSummaryDescription": "このモデルにはまだOpenCode Goの使用行がありません。", + "model.noSummaryDescription": "このモデルにはまだOpenCodeの使用行がありません。", "model.tokens": "トークン", "model.uniqueUsers": "ユニークユーザー", "model.lastTwoMonths": "過去2か月", @@ -194,15 +191,15 @@ export const dict = { "model.totalModels": "{{count}}件のモデル", "model.momentum": "勢い", "model.vsPreviousWindow": "前の期間比", - "model.usageDescription": "直近2か月の期間におけるOpenCode Goの日次トークン量。", + "model.usageDescription": "直近2か月の期間におけるOpenCodeの日次トークン量。", "model.noUsageTitle": "使用量がありません", "model.noUsageDescription": "現在の期間には使用量がありません。", "model.dailyTokenChart": "日次トークン使用量グラフ", - "model.usersDescription": "直近2か月の期間におけるOpenCode Goの日次ユニークユーザー。", + "model.usersDescription": "直近2か月の期間におけるOpenCodeの日次ユニークユーザー。", "model.noUsersTitle": "ユーザーデータがありません", "model.noUsersDescription": "現在の期間にはユーザー付き行がありません。", "model.dailyUserChart": "日次ユニークユーザーグラフ", - "model.efficiencyDescription": "コスト、キャッシュ動作、平均的なOpenCode Goセッションの形。", + "model.efficiencyDescription": "コスト、キャッシュ動作、平均的なOpenCodeセッションの形。", "model.noEfficiencyTitle": "効率データがありません", "model.noEfficiencyDescription": "使用量が届くと効率データが表示されます。", "model.totalSpendLabel": "Total spend", @@ -220,11 +217,9 @@ export const dict = { "model.tokensSession": "トークン / セッション", "model.cacheRatio": "キャッシュ比率", "model.inputTokens": "入力トークン", - "model.geoDescription": "国別のOpenCode Goモデルのトークン使用量。", "model.noGeoTitle": "地域データがありません", - "model.noGeoDescription": "このモデルに一致するOpenCode Go地域行はありません。", - "model.worldMap": "国別モデル別トークン使用量の世界地図", - "model.peersDescription": "最近のOpenCode Goトークン量が近いモデル。", + "model.noGeoDescription": "このモデルに一致するOpenCode地域行はありません。", + "model.peersDescription": "最近のOpenCodeトークン量が近いモデル。", "model.noPeersTitle": "類似モデルがありません", "model.noPeersDescription": "使用量が届くと類似モデルのランキングが表示されます。", "model.noUsageLastWeek": "先週の使用量なし", diff --git a/packages/stats/app/src/i18n/ko.ts b/packages/stats/app/src/i18n/ko.ts index 8a5d93dca0..d55ba5a606 100644 --- a/packages/stats/app/src/i18n/ko.ts +++ b/packages/stats/app/src/i18n/ko.ts @@ -3,7 +3,7 @@ import type { Key } from "../i18n" export const dict = { "app.title": "AI 모델 사용 순위 | OpenCode 데이터", "app.description": - "OpenCode Go에서 AI 모델이 어떻게 사용되는지 토큰 볼륨, 순위, 시장 점유율, 토큰 가격, 세션 비용, 캐시 비율, 지역별 분포까지 살펴보세요.", + "OpenCode에서 AI 모델이 어떻게 사용되는지 토큰 볼륨, 순위, 시장 점유율, 토큰 가격, 세션 비용, 캐시 비율, 지역별 분포까지 살펴보세요.", "app.unfurlAlt": "어두운 패턴 배경 위의 OpenCode Data 워드마크", "nav.dataHome": "데이터 홈", "nav.topModels": "상위 모델", @@ -88,7 +88,7 @@ export const dict = { "home.loadingTitle": "데이터 로딩 중", "home.loadingDescription": "모델 집계를 읽는 중입니다.", "home.usageTitle": "사용량", - "home.topModelsDescription": "OpenCode Go 전반의 모델 사용량입니다.", + "home.topModelsDescription": "OpenCode 전반의 모델 사용량입니다.", "home.noUsageTitle": "사용 데이터 없음", "home.noUsageDescription": "이 제품과 범위에 맞는 모델 행이 없습니다.", "home.noLeaderboardTitle": "리더보드 데이터 없음", @@ -100,7 +100,7 @@ export const dict = { "home.modelUsers": "모델 사용자", "home.new": "신규", "home.uniqueUsersTitle": "순 사용자", - "home.uniqueUsersDescription": "모델별 일간 OpenCode Go 순 사용자입니다.", + "home.uniqueUsersDescription": "모델별 일간 OpenCode 순 사용자입니다.", "home.noUserDataTitle": "사용자 데이터 없음", "home.noUserDataDescription": "이 기간에 맞는 사용자 포함 모델 행이 없습니다.", "home.uniqueUsersChart": "모델별 순 사용자 누적 차트", @@ -111,11 +111,8 @@ export const dict = { "home.marketChart": "모델 작성자별 시장 점유율", "home.noData": "데이터 없음", "home.geoTitle": "지역별 분포", - "home.geoDescription": "국가별 사용 토큰입니다.", "home.noGeoTitle": "지역 데이터 없음", "home.noGeoDescription": "이 범위에 맞는 지역 행이 없습니다.", - "home.worldMap": "국가별 토큰 사용량 세계 지도", - "home.geoMapTitle": "지역별 분포 지도", "home.unknown": "알 수 없음", "home.tokenCostTitle": "토큰 비용", "home.tokenCostDescription": "100만 토큰당 가격입니다.", @@ -132,7 +129,7 @@ export const dict = { "home.costSession": "비용/세션", "lab.title": "{{lab}} AI 모델 사용량 및 순위 | OpenCode 데이터", "lab.description": - "OpenCode Go에서 사용되는 {{lab}} 모델을 토큰 사용량, 모델 순위, 컨텍스트 창, 출시일, 비용, 모델별 데이터까지 비교하세요.", + "OpenCode에서 사용되는 {{lab}} 모델을 토큰 사용량, 모델 순위, 컨텍스트 창, 출시일, 비용, 모델별 데이터까지 비교하세요.", "lab.loadingTitle": "모델 랩", "lab.loadingDescription": "모델 가용성과 최근 OpenCode 사용량을 읽는 중입니다.", "lab.notFound": "이 랩과 일치하는 모델이 없습니다.", @@ -158,7 +155,7 @@ export const dict = { "lab.share": "점유율", "model.title": "{{model}} 사용량, 비용 및 순위 | OpenCode 데이터", "model.description": - "{{model}}의 OpenCode Go 사용 데이터를 토큰 볼륨, 주간 순위, 토큰 구성, 비용, 캐시 비율, 세션, 지역별 분포, 비슷한 모델까지 확인하세요.", + "{{model}}의 OpenCode 사용 데이터를 토큰 볼륨, 주간 순위, 토큰 구성, 비용, 캐시 비율, 세션, 지역별 분포, 비슷한 모델까지 확인하세요.", "model.loadingTitle": "모델 데이터", "model.loadingDescription": "모델 집계를 읽는 중입니다.", "model.loadingProfile": "모델 프로필을 읽는 중입니다.", @@ -166,25 +163,24 @@ export const dict = { "model.noDataDescription": "리더보드에서 모델을 열어보세요.", "model.noMatched": "{{id}}와 일치하는 모델 정보나 사용 행이 없습니다.", "model.fallback": "모델", - "model.catalogFallback": - "공유 모델 인덱스의 모델 정보입니다. 이 모델에 활동이 생기면 OpenCode Go 사용량이 표시됩니다.", - "model.unranked": "지난주 OpenCode Go 사용량에서 순위 없음", - "model.ranked": "지난주 OpenCode Go 사용량에서 #{{rank}}위", + "model.catalogFallback": "공유 모델 인덱스의 모델 정보입니다. 이 모델에 활동이 생기면 OpenCode 사용량이 표시됩니다.", + "model.unranked": "지난주 OpenCode 사용량에서 순위 없음", + "model.ranked": "지난주 OpenCode 사용량에서 #{{rank}}위", "model.observedVolume": "관측된 2M 볼륨의 {{share}}.", "model.weights": "모델 가중치: {{label}}", "model.rank": "7일 순위", "model.profile": "모델 프로필", "model.listed": "등록됨", - "model.noCurrentUsage": "현재 데이터 기간에는 OpenCode Go 사용량이 없습니다.", + "model.noCurrentUsage": "현재 데이터 기간에는 OpenCode 사용량이 없습니다.", "model.facts": "모델 정보", "model.context": "컨텍스트", "model.output": "출력", "model.knowledge": "지식", "model.release": "출시", "model.inputs": "입력", - "model.overviewDescription": "최근 OpenCode Go 토큰, 순 사용자, 시장 위치입니다.", + "model.overviewDescription": "최근 OpenCode 토큰, 순 사용자, 시장 위치입니다.", "model.noSummaryTitle": "사용 요약 없음", - "model.noSummaryDescription": "이 모델에는 아직 OpenCode Go 사용 행이 없습니다.", + "model.noSummaryDescription": "이 모델에는 아직 OpenCode 사용 행이 없습니다.", "model.tokens": "토큰", "model.uniqueUsers": "순 사용자", "model.lastTwoMonths": "지난 두 달", @@ -194,15 +190,15 @@ export const dict = { "model.totalModels": "{{count}}개 모델", "model.momentum": "모멘텀", "model.vsPreviousWindow": "이전 기간 대비", - "model.usageDescription": "최근 두 달 기간의 일간 OpenCode Go 토큰 볼륨입니다.", + "model.usageDescription": "최근 두 달 기간의 일간 OpenCode 토큰 볼륨입니다.", "model.noUsageTitle": "사용량 없음", "model.noUsageDescription": "현재 기간에는 사용량이 없습니다.", "model.dailyTokenChart": "일간 토큰 사용량 차트", - "model.usersDescription": "최근 두 달 기간의 일간 OpenCode Go 순 사용자입니다.", + "model.usersDescription": "최근 두 달 기간의 일간 OpenCode 순 사용자입니다.", "model.noUsersTitle": "사용자 데이터 없음", "model.noUsersDescription": "현재 기간에는 사용자 포함 행이 없습니다.", "model.dailyUserChart": "일간 순 사용자 차트", - "model.efficiencyDescription": "비용, 캐시 동작, 평균 OpenCode Go 세션 형태입니다.", + "model.efficiencyDescription": "비용, 캐시 동작, 평균 OpenCode 세션 형태입니다.", "model.noEfficiencyTitle": "효율 데이터 없음", "model.noEfficiencyDescription": "사용량이 들어오면 효율 데이터가 표시됩니다.", "model.totalSpendLabel": "Total spend", @@ -220,11 +216,9 @@ export const dict = { "model.tokensSession": "토큰 / 세션", "model.cacheRatio": "캐시 비율", "model.inputTokens": "입력 토큰", - "model.geoDescription": "국가별 OpenCode Go 모델 토큰 사용량입니다.", "model.noGeoTitle": "지역 데이터 없음", - "model.noGeoDescription": "이 모델과 일치하는 OpenCode Go 지역 행이 없습니다.", - "model.worldMap": "국가별 모델 토큰 사용량 세계 지도", - "model.peersDescription": "최근 OpenCode Go 토큰 볼륨이 가까운 모델입니다.", + "model.noGeoDescription": "이 모델과 일치하는 OpenCode 지역 행이 없습니다.", + "model.peersDescription": "최근 OpenCode 토큰 볼륨이 가까운 모델입니다.", "model.noPeersTitle": "비슷한 모델 없음", "model.noPeersDescription": "사용량이 들어오면 비슷한 모델 순위가 표시됩니다.", "model.noUsageLastWeek": "지난주 사용량 없음", diff --git a/packages/stats/app/src/i18n/no.ts b/packages/stats/app/src/i18n/no.ts index 2c6d957810..26ec3e80bf 100644 --- a/packages/stats/app/src/i18n/no.ts +++ b/packages/stats/app/src/i18n/no.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "Rangeringer for bruk av AI-modeller | OpenCode Data", "app.description": - "Utforsk bruk av OpenCode Go på tvers av AI-modeller, inkludert tokenvolum, rangeringer, markedsandel, tokenpriser, øktkostnad, cacheandel og geografiske fordelinger.", + "Utforsk bruk av OpenCode på tvers av AI-modeller, inkludert tokenvolum, rangeringer, markedsandel, tokenpriser, øktkostnad, cacheandel og geografiske fordelinger.", "app.unfurlAlt": "OpenCode Data-ordmerke på en mørk mønstret bakgrunn", "nav.dataHome": "Dataforside", "nav.topModels": "Toppmodeller", @@ -86,7 +86,7 @@ export const dict = { "home.loadingTitle": "Laster data", "home.loadingDescription": "Leser modellaggregater.", "home.usageTitle": "Bruk", - "home.topModelsDescription": "Bruk av modeller på tvers av OpenCode Go.", + "home.topModelsDescription": "Bruk av modeller på tvers av OpenCode.", "home.noUsageTitle": "Ingen bruksdata", "home.noUsageDescription": "Ingen modellrader matchet dette produktet og intervallet.", "home.noLeaderboardTitle": "Ingen rangeringsdata", @@ -98,7 +98,7 @@ export const dict = { "home.modelUsers": "modellbrukere", "home.new": "Ny", "home.uniqueUsersTitle": "Unike brukere", - "home.uniqueUsersDescription": "Daglige unike OpenCode Go-brukere etter modell.", + "home.uniqueUsersDescription": "Daglige unike OpenCode-brukere etter modell.", "home.noUserDataTitle": "Ingen brukerdata", "home.noUserDataDescription": "Ingen modellrader med brukere matchet dette vinduet.", "home.uniqueUsersChart": "Stablet diagram over unike brukere etter modell", @@ -109,11 +109,8 @@ export const dict = { "home.marketChart": "Markedsandel etter modellforfatter", "home.noData": "Ingen data", "home.geoTitle": "Geografisk fordeling", - "home.geoDescription": "Tokens brukt etter land.", "home.noGeoTitle": "Ingen geodata", "home.noGeoDescription": "Ingen georader matchet dette intervallet.", - "home.worldMap": "Verdenskart over tokenbruk etter land", - "home.geoMapTitle": "Kart over geografisk fordeling", "home.unknown": "Ukjent", "home.tokenCostTitle": "Tokenkostnad", "home.tokenCostDescription": "Pris per 1 mill. tokens.", @@ -130,7 +127,7 @@ export const dict = { "home.costSession": "Kostnad/økt", "lab.title": "{{lab}} AI-modellbruk og rangeringer | OpenCode Data", "lab.description": - "Sammenlign {{lab}}-modeller brukt i OpenCode Go, inkludert tokenbruk, modellrangeringer, kontekstvinduer, utgivelsesdatoer, kostnader og modellspesifikke data.", + "Sammenlign {{lab}}-modeller brukt i OpenCode, inkludert tokenbruk, modellrangeringer, kontekstvinduer, utgivelsesdatoer, kostnader og modellspesifikke data.", "lab.loadingTitle": "Modellab", "lab.loadingDescription": "Leser modelltilgjengelighet og nylig OpenCode-bruk.", "lab.notFound": "Ingen modeller matchet dette laben.", @@ -156,7 +153,7 @@ export const dict = { "lab.share": "Andel", "model.title": "{{model}} bruk, kostnad og rangering | OpenCode Data", "model.description": - "Se OpenCode Go-bruksdata for {{model}}, inkludert tokenvolum, ukentlig rangering, tokenmiks, kostnader, cacheandel, økter, geografiske fordelinger og lignende modeller.", + "Se OpenCode-bruksdata for {{model}}, inkludert tokenvolum, ukentlig rangering, tokenmiks, kostnader, cacheandel, økter, geografiske fordelinger og lignende modeller.", "model.loadingTitle": "Modelldata", "model.loadingDescription": "Leser modellaggregater.", "model.loadingProfile": "Leser modellprofilen.", @@ -165,24 +162,24 @@ export const dict = { "model.noMatched": "Ingen modellfakta eller bruksrader matchet {{id}}.", "model.fallback": "Modell", "model.catalogFallback": - "Modellfakta fra den delte modellindeksen. OpenCode Go-bruk vises når denne modellen har aktivitet.", - "model.unranked": "Ikke rangert i forrige ukes OpenCode Go-bruk", - "model.ranked": "Rangert #{{rank}} i forrige ukes OpenCode Go-bruk", + "Modellfakta fra den delte modellindeksen. OpenCode-bruk vises når denne modellen har aktivitet.", + "model.unranked": "Ikke rangert i forrige ukes OpenCode-bruk", + "model.ranked": "Rangert #{{rank}} i forrige ukes OpenCode-bruk", "model.observedVolume": "med {{share}} av observert 2M-volum.", "model.weights": "Modellvekter: {{label}}", "model.rank": "7D-rangering", "model.profile": "Modellprofil", "model.listed": "Oppført", - "model.noCurrentUsage": "Ingen OpenCode Go-bruk i gjeldende datavindu.", + "model.noCurrentUsage": "Ingen OpenCode-bruk i gjeldende datavindu.", "model.facts": "Modellfakta", "model.context": "Kontekst", "model.output": "Utdata", "model.knowledge": "Kunnskap", "model.release": "Utgivelse", "model.inputs": "Inndata", - "model.overviewDescription": "Nylige OpenCode Go-tokens, unike brukere og markedsposisjon.", + "model.overviewDescription": "Nylige OpenCode-tokens, unike brukere og markedsposisjon.", "model.noSummaryTitle": "Ingen bruksoversikt", - "model.noSummaryDescription": "Denne modellen har ingen OpenCode Go-bruksrader ennå.", + "model.noSummaryDescription": "Denne modellen har ingen OpenCode-bruksrader ennå.", "model.tokens": "Tokens", "model.uniqueUsers": "Unike brukere", "model.lastTwoMonths": "siste to måneder", @@ -192,15 +189,15 @@ export const dict = { "model.totalModels": "{{count}} modeller", "model.momentum": "Momentum", "model.vsPreviousWindow": "mot forrige vindu", - "model.usageDescription": "Daglig OpenCode Go-tokenvolum i det nylige tomånedersvinduet.", + "model.usageDescription": "Daglig OpenCode-tokenvolum i det nylige tomånedersvinduet.", "model.noUsageTitle": "Ingen bruk", "model.noUsageDescription": "Ingen bruk landet i gjeldende vindu.", "model.dailyTokenChart": "Daglig diagram over tokenbruk", - "model.usersDescription": "Daglige unike OpenCode Go-brukere i det nylige tomånedersvinduet.", + "model.usersDescription": "Daglige unike OpenCode-brukere i det nylige tomånedersvinduet.", "model.noUsersTitle": "Ingen brukerdata", "model.noUsersDescription": "Ingen rader med brukere landet i gjeldende vindu.", "model.dailyUserChart": "Daglig diagram over unike brukere", - "model.efficiencyDescription": "Kostnad, cacheatferd og gjennomsnittlig OpenCode Go-øktform.", + "model.efficiencyDescription": "Kostnad, cacheatferd og gjennomsnittlig OpenCode-øktform.", "model.noEfficiencyTitle": "Ingen effektivitetsdata", "model.noEfficiencyDescription": "Effektivitetsdata vises etter at bruk lander.", "model.totalSpendLabel": "Total spend", @@ -218,11 +215,9 @@ export const dict = { "model.tokensSession": "Tokens / økt", "model.cacheRatio": "Cacheandel", "model.inputTokens": "inndata-tokens", - "model.geoDescription": "OpenCode Go-modelltokens brukt etter land.", "model.noGeoTitle": "Ingen geodata", - "model.noGeoDescription": "Ingen OpenCode Go-georader matchet denne modellen.", - "model.worldMap": "Verdenskart over modelltokenbruk etter land", - "model.peersDescription": "Nærliggende modeller etter nylig OpenCode Go-tokenvolum.", + "model.noGeoDescription": "Ingen OpenCode-georader matchet denne modellen.", + "model.peersDescription": "Nærliggende modeller etter nylig OpenCode-tokenvolum.", "model.noPeersTitle": "Ingen lignende modeller", "model.noPeersDescription": "Rangeringer for lignende modeller vises etter at bruk lander.", "model.noUsageLastWeek": "Ingen bruk forrige uke", diff --git a/packages/stats/app/src/i18n/pl.ts b/packages/stats/app/src/i18n/pl.ts index d61f40cda3..e82ddeff0b 100644 --- a/packages/stats/app/src/i18n/pl.ts +++ b/packages/stats/app/src/i18n/pl.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "Rankingi użycia modeli AI | OpenCode Data", "app.description": - "Przeglądaj użycie OpenCode Go w modelach AI, w tym wolumen tokenów, rankingi, udział w rynku, ceny tokenów, koszt sesji, współczynnik cache i podziały geograficzne.", + "Przeglądaj użycie OpenCode w modelach AI, w tym wolumen tokenów, rankingi, udział w rynku, ceny tokenów, koszt sesji, współczynnik cache i podziały geograficzne.", "app.unfurlAlt": "Znak słowny OpenCode Data na ciemnym wzorzystym tle", "nav.dataHome": "Strona danych", "nav.topModels": "Najlepsze modele", @@ -85,7 +85,7 @@ export const dict = { "home.loadingTitle": "Ładowanie danych", "home.loadingDescription": "Odczytywanie agregatów modeli.", "home.usageTitle": "Użycie", - "home.topModelsDescription": "Użycie modeli w OpenCode Go.", + "home.topModelsDescription": "Użycie modeli w OpenCode.", "home.noUsageTitle": "Brak danych użycia", "home.noUsageDescription": "Żadne wiersze modeli nie pasowały do tego produktu i zakresu.", "home.noLeaderboardTitle": "Brak danych rankingu", @@ -97,7 +97,7 @@ export const dict = { "home.modelUsers": "użytkownicy modelu", "home.new": "Nowy", "home.uniqueUsersTitle": "Unikalni użytkownicy", - "home.uniqueUsersDescription": "Dzienni unikalni użytkownicy OpenCode Go według modelu.", + "home.uniqueUsersDescription": "Dzienni unikalni użytkownicy OpenCode według modelu.", "home.noUserDataTitle": "Brak danych użytkowników", "home.noUserDataDescription": "Żadne wiersze modeli z użytkownikami nie pasowały do tego okna.", "home.uniqueUsersChart": "Skumulowany wykres unikalnych użytkowników według modelu", @@ -108,11 +108,8 @@ export const dict = { "home.marketChart": "Udział w rynku według autora modelu", "home.noData": "Brak danych", "home.geoTitle": "Podział geograficzny", - "home.geoDescription": "Tokeny użyte według kraju.", "home.noGeoTitle": "Brak danych geograficznych", "home.noGeoDescription": "Żadne wiersze geograficzne nie pasowały do tego zakresu.", - "home.worldMap": "Mapa świata użycia tokenów według kraju", - "home.geoMapTitle": "Mapa podziału geograficznego", "home.unknown": "Nieznane", "home.tokenCostTitle": "Koszt tokenów", "home.tokenCostDescription": "Cena za 1 mln tokenów.", @@ -129,7 +126,7 @@ export const dict = { "home.costSession": "Koszt/sesja", "lab.title": "Użycie i rankingi modeli AI {{lab}} | OpenCode Data", "lab.description": - "Porównaj modele {{lab}} używane w OpenCode Go, w tym użycie tokenów, rankingi modeli, okna kontekstu, daty wydań, koszty i dane specyficzne dla modeli.", + "Porównaj modele {{lab}} używane w OpenCode, w tym użycie tokenów, rankingi modeli, okna kontekstu, daty wydań, koszty i dane specyficzne dla modeli.", "lab.loadingTitle": "Laboratorium modeli", "lab.loadingDescription": "Odczytywanie dostępności modeli i ostatniego użycia OpenCode.", "lab.notFound": "Żadne modele nie pasowały do tego laboratorium.", @@ -155,7 +152,7 @@ export const dict = { "lab.share": "Udział", "model.title": "Użycie, koszt i pozycja {{model}} | OpenCode Data", "model.description": - "Zobacz dane użycia {{model}} w OpenCode Go, w tym wolumen tokenów, tygodniową pozycję, miks tokenów, koszty, współczynnik cache, sesje, podziały geograficzne i podobne modele.", + "Zobacz dane użycia {{model}} w OpenCode, w tym wolumen tokenów, tygodniową pozycję, miks tokenów, koszty, współczynnik cache, sesje, podziały geograficzne i podobne modele.", "model.loadingTitle": "Dane modelu", "model.loadingDescription": "Odczytywanie agregatów modelu.", "model.loadingProfile": "Odczytywanie profilu modelu.", @@ -164,24 +161,24 @@ export const dict = { "model.noMatched": "Żadne fakty modelu ani wiersze użycia nie pasowały do {{id}}.", "model.fallback": "Model", "model.catalogFallback": - "Fakty modelu ze wspólnego indeksu modeli. Użycie OpenCode Go pojawi się, gdy ten model będzie mieć aktywność.", - "model.unranked": "Bez pozycji w użyciu OpenCode Go z zeszłego tygodnia", - "model.ranked": "Pozycja #{{rank}} w użyciu OpenCode Go z zeszłego tygodnia", + "Fakty modelu ze wspólnego indeksu modeli. Użycie OpenCode pojawi się, gdy ten model będzie mieć aktywność.", + "model.unranked": "Bez pozycji w użyciu OpenCode z zeszłego tygodnia", + "model.ranked": "Pozycja #{{rank}} w użyciu OpenCode z zeszłego tygodnia", "model.observedVolume": "z {{share}} obserwowanego wolumenu 2M.", "model.weights": "Wagi modelu: {{label}}", "model.rank": "Pozycja 7D", "model.profile": "Profil modelu", "model.listed": "W katalogu", - "model.noCurrentUsage": "Brak użycia OpenCode Go w bieżącym oknie danych.", + "model.noCurrentUsage": "Brak użycia OpenCode w bieżącym oknie danych.", "model.facts": "Fakty modelu", "model.context": "Kontekst", "model.output": "Wyjście", "model.knowledge": "Wiedza", "model.release": "Wydanie", "model.inputs": "Wejścia", - "model.overviewDescription": "Ostatnie tokeny OpenCode Go, unikalni użytkownicy i pozycja rynkowa.", + "model.overviewDescription": "Ostatnie tokeny OpenCode, unikalni użytkownicy i pozycja rynkowa.", "model.noSummaryTitle": "Brak podsumowania użycia", - "model.noSummaryDescription": "Ten model nie ma jeszcze wierszy użycia OpenCode Go.", + "model.noSummaryDescription": "Ten model nie ma jeszcze wierszy użycia OpenCode.", "model.tokens": "Tokeny", "model.uniqueUsers": "Unikalni użytkownicy", "model.lastTwoMonths": "ostatnie dwa miesiące", @@ -191,15 +188,15 @@ export const dict = { "model.totalModels": "{{count}} modeli", "model.momentum": "Momentum", "model.vsPreviousWindow": "vs poprzednie okno", - "model.usageDescription": "Dzienny wolumen tokenów OpenCode Go w ostatnim dwumiesięcznym oknie.", + "model.usageDescription": "Dzienny wolumen tokenów OpenCode w ostatnim dwumiesięcznym oknie.", "model.noUsageTitle": "Brak użycia", "model.noUsageDescription": "W bieżącym oknie nie pojawiło się użycie.", "model.dailyTokenChart": "Dzienny wykres użycia tokenów", - "model.usersDescription": "Dzienni unikalni użytkownicy OpenCode Go w ostatnim dwumiesięcznym oknie.", + "model.usersDescription": "Dzienni unikalni użytkownicy OpenCode w ostatnim dwumiesięcznym oknie.", "model.noUsersTitle": "Brak danych użytkowników", "model.noUsersDescription": "W bieżącym oknie nie pojawiły się wiersze z użytkownikami.", "model.dailyUserChart": "Dzienny wykres unikalnych użytkowników", - "model.efficiencyDescription": "Koszt, zachowanie cache i średni kształt sesji OpenCode Go.", + "model.efficiencyDescription": "Koszt, zachowanie cache i średni kształt sesji OpenCode.", "model.noEfficiencyTitle": "Brak danych wydajności", "model.noEfficiencyDescription": "Dane wydajności pojawią się po nadejściu użycia.", "model.totalSpendLabel": "Total spend", @@ -217,11 +214,9 @@ export const dict = { "model.tokensSession": "Tokeny / sesja", "model.cacheRatio": "Współczynnik cache", "model.inputTokens": "tokeny wejściowe", - "model.geoDescription": "Tokeny modelu OpenCode Go użyte według kraju.", "model.noGeoTitle": "Brak danych geograficznych", - "model.noGeoDescription": "Żadne wiersze geograficzne OpenCode Go nie pasowały do tego modelu.", - "model.worldMap": "Mapa świata użycia tokenów modelu według kraju", - "model.peersDescription": "Pobliskie modele według ostatniego wolumenu tokenów OpenCode Go.", + "model.noGeoDescription": "Żadne wiersze geograficzne OpenCode nie pasowały do tego modelu.", + "model.peersDescription": "Pobliskie modele według ostatniego wolumenu tokenów OpenCode.", "model.noPeersTitle": "Brak podobnych modeli", "model.noPeersDescription": "Rankingi podobnych modeli pojawią się po nadejściu użycia.", "model.noUsageLastWeek": "Brak użycia w zeszłym tygodniu", diff --git a/packages/stats/app/src/i18n/ru.ts b/packages/stats/app/src/i18n/ru.ts index d0bfeacc84..fe850a3094 100644 --- a/packages/stats/app/src/i18n/ru.ts +++ b/packages/stats/app/src/i18n/ru.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "Рейтинги использования ИИ-моделей | OpenCode Data", "app.description": - "Изучайте использование OpenCode Go по ИИ-моделям: объем токенов, рейтинги, долю рынка, цены токенов, стоимость сеанса, долю кэша и географические разрезы.", + "Изучайте использование OpenCode по ИИ-моделям: объем токенов, рейтинги, долю рынка, цены токенов, стоимость сеанса, долю кэша и географические разрезы.", "app.unfurlAlt": "Словесный знак OpenCode Data на темном узорном фоне", "nav.dataHome": "Главная данных", "nav.topModels": "Лучшие модели", @@ -86,7 +86,7 @@ export const dict = { "home.loadingTitle": "Загрузка данных", "home.loadingDescription": "Читаем агрегаты моделей.", "home.usageTitle": "Использование", - "home.topModelsDescription": "Использование моделей в OpenCode Go.", + "home.topModelsDescription": "Использование моделей в OpenCode.", "home.noUsageTitle": "Нет данных использования", "home.noUsageDescription": "Нет строк моделей для этого продукта и диапазона.", "home.noLeaderboardTitle": "Нет данных рейтинга", @@ -98,7 +98,7 @@ export const dict = { "home.modelUsers": "пользователи модели", "home.new": "Новая", "home.uniqueUsersTitle": "Уникальные пользователи", - "home.uniqueUsersDescription": "Ежедневные уникальные пользователи OpenCode Go по моделям.", + "home.uniqueUsersDescription": "Ежедневные уникальные пользователи OpenCode по моделям.", "home.noUserDataTitle": "Нет данных пользователей", "home.noUserDataDescription": "Нет строк моделей с пользователями для этого окна.", "home.uniqueUsersChart": "Составной график уникальных пользователей по моделям", @@ -109,11 +109,8 @@ export const dict = { "home.marketChart": "Доля рынка по автору модели", "home.noData": "Нет данных", "home.geoTitle": "Географический разрез", - "home.geoDescription": "Токены, использованные по странам.", "home.noGeoTitle": "Нет геоданных", "home.noGeoDescription": "Нет географических строк для этого диапазона.", - "home.worldMap": "Карта мира использования токенов по странам", - "home.geoMapTitle": "Карта географического разреза", "home.unknown": "Неизвестно", "home.tokenCostTitle": "Стоимость токенов", "home.tokenCostDescription": "Цена за 1 млн токенов.", @@ -130,7 +127,7 @@ export const dict = { "home.costSession": "Стоимость/сеанс", "lab.title": "Использование и рейтинги ИИ-моделей {{lab}} | OpenCode Data", "lab.description": - "Сравните модели {{lab}}, используемые в OpenCode Go: использование токенов, рейтинги моделей, контекстные окна, даты релизов, стоимость и данные конкретных моделей.", + "Сравните модели {{lab}}, используемые в OpenCode: использование токенов, рейтинги моделей, контекстные окна, даты релизов, стоимость и данные конкретных моделей.", "lab.loadingTitle": "Лаборатория моделей", "lab.loadingDescription": "Читаем доступность моделей и недавнее использование OpenCode.", "lab.notFound": "Нет моделей, соответствующих этой лаборатории.", @@ -157,7 +154,7 @@ export const dict = { "lab.share": "Доля", "model.title": "Использование, стоимость и ранг {{model}} | OpenCode Data", "model.description": - "Посмотрите данные использования {{model}} в OpenCode Go: объем токенов, недельный ранг, состав токенов, стоимость, долю кэша, сеансы, географические разрезы и похожие модели.", + "Посмотрите данные использования {{model}} в OpenCode: объем токенов, недельный ранг, состав токенов, стоимость, долю кэша, сеансы, географические разрезы и похожие модели.", "model.loadingTitle": "Данные модели", "model.loadingDescription": "Читаем агрегаты модели.", "model.loadingProfile": "Читаем профиль модели.", @@ -166,24 +163,24 @@ export const dict = { "model.noMatched": "Нет фактов модели или строк использования для {{id}}.", "model.fallback": "Модель", "model.catalogFallback": - "Факты модели из общего индекса моделей. Использование OpenCode Go появится, когда у этой модели будет активность.", - "model.unranked": "Нет рейтинга по использованию OpenCode Go за прошлую неделю", - "model.ranked": "Ранг #{{rank}} по использованию OpenCode Go за прошлую неделю", + "Факты модели из общего индекса моделей. Использование OpenCode появится, когда у этой модели будет активность.", + "model.unranked": "Нет рейтинга по использованию OpenCode за прошлую неделю", + "model.ranked": "Ранг #{{rank}} по использованию OpenCode за прошлую неделю", "model.observedVolume": "с {{share}} наблюдаемого объема 2M.", "model.weights": "Веса модели: {{label}}", "model.rank": "Ранг за 7 дн.", "model.profile": "Профиль модели", "model.listed": "В каталоге", - "model.noCurrentUsage": "Нет использования OpenCode Go в текущем окне данных.", + "model.noCurrentUsage": "Нет использования OpenCode в текущем окне данных.", "model.facts": "Факты модели", "model.context": "Контекст", "model.output": "Вывод", "model.knowledge": "Знания", "model.release": "Релиз", "model.inputs": "Входы", - "model.overviewDescription": "Недавние токены OpenCode Go, уникальные пользователи и рыночная позиция.", + "model.overviewDescription": "Недавние токены OpenCode, уникальные пользователи и рыночная позиция.", "model.noSummaryTitle": "Нет сводки использования", - "model.noSummaryDescription": "У этой модели пока нет строк использования OpenCode Go.", + "model.noSummaryDescription": "У этой модели пока нет строк использования OpenCode.", "model.tokens": "Токены", "model.uniqueUsers": "Уникальные пользователи", "model.lastTwoMonths": "последние два месяца", @@ -193,15 +190,15 @@ export const dict = { "model.totalModels": "{{count}} моделей", "model.momentum": "Динамика", "model.vsPreviousWindow": "к предыдущему окну", - "model.usageDescription": "Ежедневный объем токенов OpenCode Go за недавнее двухмесячное окно.", + "model.usageDescription": "Ежедневный объем токенов OpenCode за недавнее двухмесячное окно.", "model.noUsageTitle": "Нет использования", "model.noUsageDescription": "В текущем окне использование не появилось.", "model.dailyTokenChart": "Ежедневный график использования токенов", - "model.usersDescription": "Ежедневные уникальные пользователи OpenCode Go за недавнее двухмесячное окно.", + "model.usersDescription": "Ежедневные уникальные пользователи OpenCode за недавнее двухмесячное окно.", "model.noUsersTitle": "Нет данных пользователей", "model.noUsersDescription": "В текущем окне нет строк с пользователями.", "model.dailyUserChart": "Ежедневный график уникальных пользователей", - "model.efficiencyDescription": "Стоимость, поведение кэша и средняя форма сеанса OpenCode Go.", + "model.efficiencyDescription": "Стоимость, поведение кэша и средняя форма сеанса OpenCode.", "model.noEfficiencyTitle": "Нет данных эффективности", "model.noEfficiencyDescription": "Данные эффективности появятся после использования.", "model.totalSpendLabel": "Total spend", @@ -219,11 +216,9 @@ export const dict = { "model.tokensSession": "Токены / сеанс", "model.cacheRatio": "Доля кэша", "model.inputTokens": "входные токены", - "model.geoDescription": "Токены модели OpenCode Go, использованные по странам.", "model.noGeoTitle": "Нет геоданных", - "model.noGeoDescription": "Нет географических строк OpenCode Go для этой модели.", - "model.worldMap": "Карта мира использования токенов модели по странам", - "model.peersDescription": "Близкие модели по недавнему объему токенов OpenCode Go.", + "model.noGeoDescription": "Нет географических строк OpenCode для этой модели.", + "model.peersDescription": "Близкие модели по недавнему объему токенов OpenCode.", "model.noPeersTitle": "Нет похожих моделей", "model.noPeersDescription": "Рейтинги похожих моделей появятся после использования.", "model.noUsageLastWeek": "Нет использования на прошлой неделе", diff --git a/packages/stats/app/src/i18n/th.ts b/packages/stats/app/src/i18n/th.ts index 59733d5c6c..84f79a21f7 100644 --- a/packages/stats/app/src/i18n/th.ts +++ b/packages/stats/app/src/i18n/th.ts @@ -3,7 +3,7 @@ import type { Key } from "../i18n" export const dict = { "app.title": "อันดับการใช้งานโมเดล AI | ข้อมูล OpenCode", "app.description": - "สำรวจการใช้งาน OpenCode Go ในโมเดล AI ต่างๆ รวมถึงปริมาณ token อันดับ ส่วนแบ่งตลาด ราคา token ต้นทุนต่อเซสชัน อัตราแคช และข้อมูลแยกตามภูมิศาสตร์", + "สำรวจการใช้งาน OpenCode ในโมเดล AI ต่างๆ รวมถึงปริมาณ token อันดับ ส่วนแบ่งตลาด ราคา token ต้นทุนต่อเซสชัน อัตราแคช และข้อมูลแยกตามภูมิศาสตร์", "app.unfurlAlt": "เวิร์ดมาร์ก OpenCode Data บนพื้นหลังลวดลายสีเข้ม", "nav.dataHome": "หน้าหลักข้อมูล", "nav.topModels": "โมเดลยอดนิยม", @@ -87,7 +87,7 @@ export const dict = { "home.loadingTitle": "กำลังโหลดข้อมูล", "home.loadingDescription": "กำลังอ่านข้อมูลสรุปของโมเดล", "home.usageTitle": "การใช้งาน", - "home.topModelsDescription": "การใช้งานโมเดลบน OpenCode Go", + "home.topModelsDescription": "การใช้งานโมเดลบน OpenCode", "home.noUsageTitle": "ไม่มีข้อมูลการใช้งาน", "home.noUsageDescription": "ไม่มีแถวโมเดลที่ตรงกับผลิตภัณฑ์และช่วงเวลานี้", "home.noLeaderboardTitle": "ไม่มีข้อมูลลีดเดอร์บอร์ด", @@ -99,7 +99,7 @@ export const dict = { "home.modelUsers": "ผู้ใช้โมเดล", "home.new": "ใหม่", "home.uniqueUsersTitle": "ผู้ใช้ไม่ซ้ำ", - "home.uniqueUsersDescription": "ผู้ใช้ OpenCode Go ไม่ซ้ำรายวัน แยกตามโมเดล", + "home.uniqueUsersDescription": "ผู้ใช้ OpenCode ไม่ซ้ำรายวัน แยกตามโมเดล", "home.noUserDataTitle": "ไม่มีข้อมูลผู้ใช้", "home.noUserDataDescription": "ไม่มีแถวโมเดลที่มีข้อมูลผู้ใช้ตรงกับช่วงเวลานี้", "home.uniqueUsersChart": "กราฟซ้อนผู้ใช้ไม่ซ้ำแยกตามโมเดล", @@ -110,11 +110,8 @@ export const dict = { "home.marketChart": "ส่วนแบ่งตลาดตามผู้สร้างโมเดล", "home.noData": "ไม่มีข้อมูล", "home.geoTitle": "แยกตามภูมิศาสตร์", - "home.geoDescription": "token ที่ใช้แยกตามประเทศ", "home.noGeoTitle": "ไม่มีข้อมูลภูมิศาสตร์", "home.noGeoDescription": "ไม่มีแถวภูมิศาสตร์ที่ตรงกับช่วงเวลานี้", - "home.worldMap": "แผนที่โลกของการใช้ token แยกตามประเทศ", - "home.geoMapTitle": "แผนที่แยกตามภูมิศาสตร์", "home.unknown": "ไม่ทราบ", "home.tokenCostTitle": "ต้นทุน Token", "home.tokenCostDescription": "ราคาต่อ 1 ล้าน token", @@ -131,7 +128,7 @@ export const dict = { "home.costSession": "ต้นทุน/เซสชัน", "lab.title": "การใช้งานและอันดับโมเดล AI ของ {{lab}} | ข้อมูล OpenCode", "lab.description": - "เปรียบเทียบโมเดล {{lab}} ที่ใช้ใน OpenCode Go รวมถึงการใช้ token อันดับโมเดล หน้าต่างบริบท วันที่ปล่อย ต้นทุน และข้อมูลเฉพาะโมเดล", + "เปรียบเทียบโมเดล {{lab}} ที่ใช้ใน OpenCode รวมถึงการใช้ token อันดับโมเดล หน้าต่างบริบท วันที่ปล่อย ต้นทุน และข้อมูลเฉพาะโมเดล", "lab.loadingTitle": "แล็บโมเดล", "lab.loadingDescription": "กำลังอ่านความพร้อมของโมเดลและการใช้งาน OpenCode ล่าสุด", "lab.notFound": "ไม่มีโมเดลที่ตรงกับแล็บนี้", @@ -157,7 +154,7 @@ export const dict = { "lab.share": "ส่วนแบ่ง", "model.title": "การใช้งาน ต้นทุน และอันดับของ {{model}} | ข้อมูล OpenCode", "model.description": - "ดูข้อมูลการใช้งาน OpenCode Go ของ {{model}} รวมถึงปริมาณ token อันดับรายสัปดาห์ ส่วนผสม token ต้นทุน อัตราแคช เซสชัน ข้อมูลแยกตามภูมิศาสตร์ และโมเดลใกล้เคียง", + "ดูข้อมูลการใช้งาน OpenCode ของ {{model}} รวมถึงปริมาณ token อันดับรายสัปดาห์ ส่วนผสม token ต้นทุน อัตราแคช เซสชัน ข้อมูลแยกตามภูมิศาสตร์ และโมเดลใกล้เคียง", "model.loadingTitle": "ข้อมูลโมเดล", "model.loadingDescription": "กำลังอ่านข้อมูลสรุปของโมเดล", "model.loadingProfile": "กำลังอ่านโปรไฟล์โมเดล", @@ -166,24 +163,24 @@ export const dict = { "model.noMatched": "ไม่มีข้อเท็จจริงของโมเดลหรือแถวการใช้งานที่ตรงกับ {{id}}", "model.fallback": "โมเดล", "model.catalogFallback": - "ข้อเท็จจริงของโมเดลจากดัชนีโมเดลที่ใช้ร่วมกัน การใช้งาน OpenCode Go จะแสดงเมื่อโมเดลนี้มีกิจกรรม", - "model.unranked": "ไม่มีอันดับในการใช้งาน OpenCode Go ของสัปดาห์ที่แล้ว", - "model.ranked": "อันดับ #{{rank}} ในการใช้งาน OpenCode Go ของสัปดาห์ที่แล้ว", + "ข้อเท็จจริงของโมเดลจากดัชนีโมเดลที่ใช้ร่วมกัน การใช้งาน OpenCode จะแสดงเมื่อโมเดลนี้มีกิจกรรม", + "model.unranked": "ไม่มีอันดับในการใช้งาน OpenCode ของสัปดาห์ที่แล้ว", + "model.ranked": "อันดับ #{{rank}} ในการใช้งาน OpenCode ของสัปดาห์ที่แล้ว", "model.observedVolume": "พร้อม {{share}} ของปริมาณ 2M ที่สังเกตได้", "model.weights": "น้ำหนักโมเดล: {{label}}", "model.rank": "อันดับ 7 วัน", "model.profile": "โปรไฟล์โมเดล", "model.listed": "อยู่ในรายการ", - "model.noCurrentUsage": "ไม่มีการใช้งาน OpenCode Go ในช่วงข้อมูลปัจจุบัน", + "model.noCurrentUsage": "ไม่มีการใช้งาน OpenCode ในช่วงข้อมูลปัจจุบัน", "model.facts": "ข้อเท็จจริงของโมเดล", "model.context": "บริบท", "model.output": "เอาต์พุต", "model.knowledge": "ความรู้", "model.release": "รุ่น", "model.inputs": "อินพุต", - "model.overviewDescription": "token ล่าสุดของ OpenCode Go ผู้ใช้ไม่ซ้ำ และตำแหน่งในตลาด", + "model.overviewDescription": "token ล่าสุดของ OpenCode ผู้ใช้ไม่ซ้ำ และตำแหน่งในตลาด", "model.noSummaryTitle": "ไม่มีสรุปการใช้งาน", - "model.noSummaryDescription": "โมเดลนี้ยังไม่มีแถวการใช้งาน OpenCode Go", + "model.noSummaryDescription": "โมเดลนี้ยังไม่มีแถวการใช้งาน OpenCode", "model.tokens": "Token", "model.uniqueUsers": "ผู้ใช้ไม่ซ้ำ", "model.lastTwoMonths": "สองเดือนล่าสุด", @@ -193,15 +190,15 @@ export const dict = { "model.totalModels": "{{count}} โมเดล", "model.momentum": "โมเมนตัม", "model.vsPreviousWindow": "เทียบกับช่วงก่อนหน้า", - "model.usageDescription": "ปริมาณ token รายวันของ OpenCode Go ในช่วงสองเดือนล่าสุด", + "model.usageDescription": "ปริมาณ token รายวันของ OpenCode ในช่วงสองเดือนล่าสุด", "model.noUsageTitle": "ไม่มีการใช้งาน", "model.noUsageDescription": "ไม่มีการใช้งานในช่วงปัจจุบัน", "model.dailyTokenChart": "กราฟการใช้ token รายวัน", - "model.usersDescription": "ผู้ใช้ OpenCode Go ไม่ซ้ำรายวันในช่วงสองเดือนล่าสุด", + "model.usersDescription": "ผู้ใช้ OpenCode ไม่ซ้ำรายวันในช่วงสองเดือนล่าสุด", "model.noUsersTitle": "ไม่มีข้อมูลผู้ใช้", "model.noUsersDescription": "ไม่มีแถวที่มีข้อมูลผู้ใช้ในช่วงปัจจุบัน", "model.dailyUserChart": "กราฟผู้ใช้ไม่ซ้ำรายวัน", - "model.efficiencyDescription": "ต้นทุน พฤติกรรมแคช และรูปแบบเซสชัน OpenCode Go โดยเฉลี่ย", + "model.efficiencyDescription": "ต้นทุน พฤติกรรมแคช และรูปแบบเซสชัน OpenCode โดยเฉลี่ย", "model.noEfficiencyTitle": "ไม่มีข้อมูลประสิทธิภาพ", "model.noEfficiencyDescription": "ข้อมูลประสิทธิภาพจะแสดงหลังจากมีการใช้งานเข้ามา", "model.totalSpendLabel": "Total spend", @@ -219,11 +216,9 @@ export const dict = { "model.tokensSession": "Token / เซสชัน", "model.cacheRatio": "อัตราแคช", "model.inputTokens": "input token", - "model.geoDescription": "token ของโมเดล OpenCode Go ที่ใช้แยกตามประเทศ", "model.noGeoTitle": "ไม่มีข้อมูลภูมิศาสตร์", - "model.noGeoDescription": "ไม่มีแถวภูมิศาสตร์ของ OpenCode Go ที่ตรงกับโมเดลนี้", - "model.worldMap": "แผนที่โลกของการใช้ token ของโมเดลแยกตามประเทศ", - "model.peersDescription": "โมเดลใกล้เคียงตามปริมาณ token ล่าสุดของ OpenCode Go", + "model.noGeoDescription": "ไม่มีแถวภูมิศาสตร์ของ OpenCode ที่ตรงกับโมเดลนี้", + "model.peersDescription": "โมเดลใกล้เคียงตามปริมาณ token ล่าสุดของ OpenCode", "model.noPeersTitle": "ไม่มีโมเดลใกล้เคียง", "model.noPeersDescription": "อันดับโมเดลใกล้เคียงจะแสดงหลังจากมีการใช้งานเข้ามา", "model.noUsageLastWeek": "ไม่มีการใช้งานเมื่อสัปดาห์ที่แล้ว", diff --git a/packages/stats/app/src/i18n/tr.ts b/packages/stats/app/src/i18n/tr.ts index a9961943ea..27926c9d54 100644 --- a/packages/stats/app/src/i18n/tr.ts +++ b/packages/stats/app/src/i18n/tr.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "Yapay Zeka Model Kullanım Sıralamaları | OpenCode Data", "app.description": - "OpenCode Go kullanımını yapay zeka modelleri genelinde keşfedin: token hacmi, sıralamalar, pazar payı, token fiyatlandırması, oturum maliyeti, önbellek oranı ve coğrafi dağılımlar.", + "OpenCode kullanımını yapay zeka modelleri genelinde keşfedin: token hacmi, sıralamalar, pazar payı, token fiyatlandırması, oturum maliyeti, önbellek oranı ve coğrafi dağılımlar.", "app.unfurlAlt": "Koyu desenli arka plan üzerinde OpenCode Data logosu", "nav.dataHome": "Veri Ana Sayfası", "nav.topModels": "En İyi Modeller", @@ -86,7 +86,7 @@ export const dict = { "home.loadingTitle": "Veriler yükleniyor", "home.loadingDescription": "Model toplamları okunuyor.", "home.usageTitle": "Kullanım", - "home.topModelsDescription": "OpenCode Go genelinde model kullanımı.", + "home.topModelsDescription": "OpenCode genelinde model kullanımı.", "home.noUsageTitle": "Kullanım verisi yok", "home.noUsageDescription": "Bu ürün ve aralıkla eşleşen model satırı yok.", "home.noLeaderboardTitle": "Sıralama verisi yok", @@ -98,7 +98,7 @@ export const dict = { "home.modelUsers": "model kullanıcıları", "home.new": "Yeni", "home.uniqueUsersTitle": "Benzersiz Kullanıcılar", - "home.uniqueUsersDescription": "Modele göre günlük benzersiz OpenCode Go kullanıcıları.", + "home.uniqueUsersDescription": "Modele göre günlük benzersiz OpenCode kullanıcıları.", "home.noUserDataTitle": "Kullanıcı verisi yok", "home.noUserDataDescription": "Bu pencereyle eşleşen kullanıcı içeren model satırı yok.", "home.uniqueUsersChart": "Modele göre yığılmış benzersiz kullanıcı grafiği", @@ -109,11 +109,8 @@ export const dict = { "home.marketChart": "Model yazarına göre pazar payı", "home.noData": "Veri yok", "home.geoTitle": "Coğrafi Dağılım", - "home.geoDescription": "Ülkeye göre kullanılan tokenlar.", "home.noGeoTitle": "Coğrafi veri yok", "home.noGeoDescription": "Bu aralıkla eşleşen coğrafi satır yok.", - "home.worldMap": "Ülkeye göre token kullanımının dünya haritası", - "home.geoMapTitle": "Coğrafi Dağılım haritası", "home.unknown": "Bilinmiyor", "home.tokenCostTitle": "Token Maliyeti", "home.tokenCostDescription": "1 milyon token başına fiyat.", @@ -130,7 +127,7 @@ export const dict = { "home.costSession": "Maliyet/Oturum", "lab.title": "{{lab}} Yapay Zeka Model Kullanımı ve Sıralamaları | OpenCode Data", "lab.description": - "OpenCode Go'da kullanılan {{lab}} modellerini karşılaştırın: token kullanımı, model sıralamaları, bağlam pencereleri, yayın tarihleri, maliyetler ve modele özel veriler.", + "OpenCode'da kullanılan {{lab}} modellerini karşılaştırın: token kullanımı, model sıralamaları, bağlam pencereleri, yayın tarihleri, maliyetler ve modele özel veriler.", "lab.loadingTitle": "Model Laboratuvarı", "lab.loadingDescription": "Model kullanılabilirliği ve son OpenCode kullanımı okunuyor.", "lab.notFound": "Bu laboratuvarla eşleşen model yok.", @@ -157,7 +154,7 @@ export const dict = { "lab.share": "Pay", "model.title": "{{model}} Kullanımı, Maliyeti ve Sırası | OpenCode Data", "model.description": - "{{model}} için OpenCode Go kullanım verilerini görüntüleyin: token hacmi, haftalık sıra, token karışımı, maliyetler, önbellek oranı, oturumlar, coğrafi dağılımlar ve benzer modeller.", + "{{model}} için OpenCode kullanım verilerini görüntüleyin: token hacmi, haftalık sıra, token karışımı, maliyetler, önbellek oranı, oturumlar, coğrafi dağılımlar ve benzer modeller.", "model.loadingTitle": "Model Verileri", "model.loadingDescription": "Model toplamları okunuyor.", "model.loadingProfile": "Model profili okunuyor.", @@ -166,24 +163,24 @@ export const dict = { "model.noMatched": "{{id}} ile eşleşen model bilgisi veya kullanım satırı yok.", "model.fallback": "Model", "model.catalogFallback": - "Paylaşılan model dizininden model bilgileri. Bu modelde etkinlik olduğunda OpenCode Go kullanımı görünür.", - "model.unranked": "Geçen haftaki OpenCode Go kullanımında sıralanmamış", - "model.ranked": "Geçen haftaki OpenCode Go kullanımında #{{rank}} sırada", + "Paylaşılan model dizininden model bilgileri. Bu modelde etkinlik olduğunda OpenCode kullanımı görünür.", + "model.unranked": "Geçen haftaki OpenCode kullanımında sıralanmamış", + "model.ranked": "Geçen haftaki OpenCode kullanımında #{{rank}} sırada", "model.observedVolume": "gözlemlenen 2M hacmin {{share}} payıyla.", "model.weights": "Model ağırlıkları: {{label}}", "model.rank": "7G Sıra", "model.profile": "Model Profili", "model.listed": "Listelendi", - "model.noCurrentUsage": "Geçerli veri penceresinde OpenCode Go kullanımı yok.", + "model.noCurrentUsage": "Geçerli veri penceresinde OpenCode kullanımı yok.", "model.facts": "Model bilgileri", "model.context": "Bağlam", "model.output": "Çıktı", "model.knowledge": "Bilgi", "model.release": "Yayın", "model.inputs": "Girdiler", - "model.overviewDescription": "Son OpenCode Go tokenları, benzersiz kullanıcılar ve pazar konumu.", + "model.overviewDescription": "Son OpenCode tokenları, benzersiz kullanıcılar ve pazar konumu.", "model.noSummaryTitle": "Kullanım özeti yok", - "model.noSummaryDescription": "Bu modelde henüz OpenCode Go kullanım satırı yok.", + "model.noSummaryDescription": "Bu modelde henüz OpenCode kullanım satırı yok.", "model.tokens": "Tokenlar", "model.uniqueUsers": "Benzersiz Kullanıcılar", "model.lastTwoMonths": "son iki ay", @@ -193,15 +190,15 @@ export const dict = { "model.totalModels": "{{count}} model", "model.momentum": "İvme", "model.vsPreviousWindow": "önceki pencereye göre", - "model.usageDescription": "Son iki aylık pencerede günlük OpenCode Go token hacmi.", + "model.usageDescription": "Son iki aylık pencerede günlük OpenCode token hacmi.", "model.noUsageTitle": "Kullanım yok", "model.noUsageDescription": "Geçerli pencereye kullanım gelmedi.", "model.dailyTokenChart": "Günlük token kullanım grafiği", - "model.usersDescription": "Son iki aylık pencerede günlük benzersiz OpenCode Go kullanıcıları.", + "model.usersDescription": "Son iki aylık pencerede günlük benzersiz OpenCode kullanıcıları.", "model.noUsersTitle": "Kullanıcı verisi yok", "model.noUsersDescription": "Geçerli pencereye kullanıcı içeren satır gelmedi.", "model.dailyUserChart": "Günlük benzersiz kullanıcı grafiği", - "model.efficiencyDescription": "Maliyet, önbellek davranışı ve ortalama OpenCode Go oturum yapısı.", + "model.efficiencyDescription": "Maliyet, önbellek davranışı ve ortalama OpenCode oturum yapısı.", "model.noEfficiencyTitle": "Verimlilik verisi yok", "model.noEfficiencyDescription": "Verimlilik verileri kullanım geldikten sonra görünür.", "model.totalSpendLabel": "Total spend", @@ -219,11 +216,9 @@ export const dict = { "model.tokensSession": "Token / Oturum", "model.cacheRatio": "Önbellek Oranı", "model.inputTokens": "giriş tokenları", - "model.geoDescription": "Ülkeye göre kullanılan OpenCode Go model tokenları.", "model.noGeoTitle": "Coğrafi veri yok", - "model.noGeoDescription": "Bu modelle eşleşen OpenCode Go coğrafi satırı yok.", - "model.worldMap": "Ülkeye göre model token kullanımının dünya haritası", - "model.peersDescription": "Son OpenCode Go token hacmine göre yakındaki modeller.", + "model.noGeoDescription": "Bu modelle eşleşen OpenCode coğrafi satırı yok.", + "model.peersDescription": "Son OpenCode token hacmine göre yakındaki modeller.", "model.noPeersTitle": "Benzer yok", "model.noPeersDescription": "Benzer model sıralamaları kullanım geldikten sonra görünür.", "model.noUsageLastWeek": "Geçen hafta kullanım yok", diff --git a/packages/stats/app/src/i18n/uk.ts b/packages/stats/app/src/i18n/uk.ts index 99d53acf92..eb43f81aa8 100644 --- a/packages/stats/app/src/i18n/uk.ts +++ b/packages/stats/app/src/i18n/uk.ts @@ -1,7 +1,7 @@ export const dict = { "app.title": "Рейтинги використання моделей ШІ | OpenCode Data", "app.description": - "Досліджуйте використання OpenCode Go серед моделей ШІ, зокрема обсяг токенів, рейтинги, частку ринку, ціни токенів, вартість сеансу, частку кешу та географічні розрізи.", + "Досліджуйте використання OpenCode серед моделей ШІ, зокрема обсяг токенів, рейтинги, частку ринку, ціни токенів, вартість сеансу, частку кешу та географічні розрізи.", "app.unfurlAlt": "Словесний знак OpenCode Data на темному візерунковому тлі", "nav.dataHome": "Головна даних", "nav.topModels": "Найкращі моделі", @@ -86,7 +86,7 @@ export const dict = { "home.loadingTitle": "Завантаження даних", "home.loadingDescription": "Читаємо агрегати моделей.", "home.usageTitle": "Використання", - "home.topModelsDescription": "Використання моделей в OpenCode Go.", + "home.topModelsDescription": "Використання моделей в OpenCode.", "home.noUsageTitle": "Немає даних використання", "home.noUsageDescription": "Жодні рядки моделей не відповідали цьому продукту й діапазону.", "home.noLeaderboardTitle": "Немає даних рейтингу", @@ -98,7 +98,7 @@ export const dict = { "home.modelUsers": "користувачі моделі", "home.new": "Нова", "home.uniqueUsersTitle": "Унікальні користувачі", - "home.uniqueUsersDescription": "Щоденні унікальні користувачі OpenCode Go за моделлю.", + "home.uniqueUsersDescription": "Щоденні унікальні користувачі OpenCode за моделлю.", "home.noUserDataTitle": "Немає даних користувачів", "home.noUserDataDescription": "Жодні рядки моделей із користувачами не відповідали цьому вікну.", "home.uniqueUsersChart": "Складена діаграма унікальних користувачів за моделлю", @@ -109,11 +109,8 @@ export const dict = { "home.marketChart": "Частка ринку за автором моделі", "home.noData": "Немає даних", "home.geoTitle": "Географічний розріз", - "home.geoDescription": "Токени, використані за країнами.", "home.noGeoTitle": "Немає геоданих", "home.noGeoDescription": "Жодні географічні рядки не відповідали цьому діапазону.", - "home.worldMap": "Карта світу використання токенів за країнами", - "home.geoMapTitle": "Карта географічного розрізу", "home.unknown": "Невідомо", "home.tokenCostTitle": "Вартість токенів", "home.tokenCostDescription": "Ціна за 1 млн токенів.", @@ -130,7 +127,7 @@ export const dict = { "home.costSession": "Вартість/сеанс", "lab.title": "Використання та рейтинги моделей ШІ {{lab}} | OpenCode Data", "lab.description": - "Порівняйте моделі {{lab}}, які використовуються в OpenCode Go, зокрема використання токенів, рейтинги моделей, контекстні вікна, дати релізів, вартість і дані конкретних моделей.", + "Порівняйте моделі {{lab}}, які використовуються в OpenCode, зокрема використання токенів, рейтинги моделей, контекстні вікна, дати релізів, вартість і дані конкретних моделей.", "lab.loadingTitle": "Лабораторія моделей", "lab.loadingDescription": "Читаємо доступність моделей і нещодавнє використання OpenCode.", "lab.notFound": "Жодна модель не відповідала цій лабораторії.", @@ -157,7 +154,7 @@ export const dict = { "lab.share": "Частка", "model.title": "Використання, вартість і ранг {{model}} | OpenCode Data", "model.description": - "Перегляньте дані використання {{model}} в OpenCode Go, зокрема обсяг токенів, тижневий ранг, мікс токенів, вартість, частку кешу, сеанси, географічні розрізи та схожі моделі.", + "Перегляньте дані використання {{model}} в OpenCode, зокрема обсяг токенів, тижневий ранг, мікс токенів, вартість, частку кешу, сеанси, географічні розрізи та схожі моделі.", "model.loadingTitle": "Дані моделі", "model.loadingDescription": "Читаємо агрегати моделі.", "model.loadingProfile": "Читаємо профіль моделі.", @@ -166,24 +163,24 @@ export const dict = { "model.noMatched": "Жодні факти моделі або рядки використання не відповідали {{id}}.", "model.fallback": "Модель", "model.catalogFallback": - "Факти моделі зі спільного індексу моделей. Використання OpenCode Go з'явиться, коли ця модель матиме активність.", - "model.unranked": "Без рейтингу у використанні OpenCode Go за минулий тиждень", - "model.ranked": "Ранг #{{rank}} у використанні OpenCode Go за минулий тиждень", + "Факти моделі зі спільного індексу моделей. Використання OpenCode з'явиться, коли ця модель матиме активність.", + "model.unranked": "Без рейтингу у використанні OpenCode за минулий тиждень", + "model.ranked": "Ранг #{{rank}} у використанні OpenCode за минулий тиждень", "model.observedVolume": "з {{share}} спостережуваного обсягу 2M.", "model.weights": "Ваги моделі: {{label}}", "model.rank": "Ранг за 7 дн.", "model.profile": "Профіль моделі", "model.listed": "У каталозі", - "model.noCurrentUsage": "Немає використання OpenCode Go в поточному вікні даних.", + "model.noCurrentUsage": "Немає використання OpenCode в поточному вікні даних.", "model.facts": "Факти моделі", "model.context": "Контекст", "model.output": "Вихід", "model.knowledge": "Знання", "model.release": "Реліз", "model.inputs": "Входи", - "model.overviewDescription": "Нещодавні токени OpenCode Go, унікальні користувачі та ринкова позиція.", + "model.overviewDescription": "Нещодавні токени OpenCode, унікальні користувачі та ринкова позиція.", "model.noSummaryTitle": "Немає підсумку використання", - "model.noSummaryDescription": "Ця модель ще не має рядків використання OpenCode Go.", + "model.noSummaryDescription": "Ця модель ще не має рядків використання OpenCode.", "model.tokens": "Токени", "model.uniqueUsers": "Унікальні користувачі", "model.lastTwoMonths": "останні два місяці", @@ -193,15 +190,15 @@ export const dict = { "model.totalModels": "{{count}} моделей", "model.momentum": "Динаміка", "model.vsPreviousWindow": "до попереднього вікна", - "model.usageDescription": "Щоденний обсяг токенів OpenCode Go за нещодавнє двомісячне вікно.", + "model.usageDescription": "Щоденний обсяг токенів OpenCode за нещодавнє двомісячне вікно.", "model.noUsageTitle": "Немає використання", "model.noUsageDescription": "У поточному вікні використання не з'явилося.", "model.dailyTokenChart": "Щоденна діаграма використання токенів", - "model.usersDescription": "Щоденні унікальні користувачі OpenCode Go за нещодавнє двомісячне вікно.", + "model.usersDescription": "Щоденні унікальні користувачі OpenCode за нещодавнє двомісячне вікно.", "model.noUsersTitle": "Немає даних користувачів", "model.noUsersDescription": "У поточному вікні немає рядків із користувачами.", "model.dailyUserChart": "Щоденна діаграма унікальних користувачів", - "model.efficiencyDescription": "Вартість, поведінка кешу та середня форма сеансу OpenCode Go.", + "model.efficiencyDescription": "Вартість, поведінка кешу та середня форма сеансу OpenCode.", "model.noEfficiencyTitle": "Немає даних ефективності", "model.noEfficiencyDescription": "Дані ефективності з'являться після використання.", "model.totalSpendLabel": "Total spend", @@ -219,11 +216,9 @@ export const dict = { "model.tokensSession": "Токени / сеанс", "model.cacheRatio": "Частка кешу", "model.inputTokens": "вхідні токени", - "model.geoDescription": "Токени моделі OpenCode Go, використані за країнами.", "model.noGeoTitle": "Немає геоданих", - "model.noGeoDescription": "Жодні географічні рядки OpenCode Go не відповідали цій моделі.", - "model.worldMap": "Карта світу використання токенів моделі за країнами", - "model.peersDescription": "Близькі моделі за нещодавнім обсягом токенів OpenCode Go.", + "model.noGeoDescription": "Жодні географічні рядки OpenCode не відповідали цій моделі.", + "model.peersDescription": "Близькі моделі за нещодавнім обсягом токенів OpenCode.", "model.noPeersTitle": "Немає схожих моделей", "model.noPeersDescription": "Рейтинги схожих моделей з'являться після використання.", "model.noUsageLastWeek": "Немає використання минулого тижня", diff --git a/packages/stats/app/src/i18n/zh.ts b/packages/stats/app/src/i18n/zh.ts index 7ff477c97f..f41222bf82 100644 --- a/packages/stats/app/src/i18n/zh.ts +++ b/packages/stats/app/src/i18n/zh.ts @@ -3,7 +3,7 @@ import type { Key } from "../i18n" export const dict = { "app.title": "AI 模型使用排名 | OpenCode 数据", "app.description": - "探索 OpenCode Go 在各 AI 模型上的使用情况,包括 token 用量、排名、市场份额、token 价格、会话成本、缓存比例和地理分布。", + "探索 OpenCode 在各 AI 模型上的使用情况,包括 token 用量、排名、市场份额、token 价格、会话成本、缓存比例和地理分布。", "app.unfurlAlt": "深色纹理背景上的 OpenCode Data 字标", "nav.dataHome": "数据首页", "nav.topModels": "热门模型", @@ -87,7 +87,7 @@ export const dict = { "home.loadingTitle": "正在加载数据", "home.loadingDescription": "正在读取模型汇总。", "home.usageTitle": "用量", - "home.topModelsDescription": "OpenCode Go 中各模型的使用情况。", + "home.topModelsDescription": "OpenCode 中各模型的使用情况。", "home.noUsageTitle": "无使用数据", "home.noUsageDescription": "没有符合该产品和时间范围的模型行。", "home.noLeaderboardTitle": "无排行榜数据", @@ -99,7 +99,7 @@ export const dict = { "home.modelUsers": "模型用户", "home.new": "新", "home.uniqueUsersTitle": "独立用户", - "home.uniqueUsersDescription": "按模型显示的每日 OpenCode Go 独立用户。", + "home.uniqueUsersDescription": "按模型显示的每日 OpenCode 独立用户。", "home.noUserDataTitle": "无用户数据", "home.noUserDataDescription": "没有符合该窗口且包含用户的模型行。", "home.uniqueUsersChart": "按模型显示的独立用户堆叠图", @@ -110,11 +110,8 @@ export const dict = { "home.marketChart": "按模型作者显示的市场份额", "home.noData": "无数据", "home.geoTitle": "地理分布", - "home.geoDescription": "按国家/地区统计的 token 使用量。", "home.noGeoTitle": "无地理数据", "home.noGeoDescription": "没有符合该时间范围的地理行。", - "home.worldMap": "按国家/地区显示 token 使用量的世界地图", - "home.geoMapTitle": "地理分布地图", "home.unknown": "未知", "home.tokenCostTitle": "Token 成本", "home.tokenCostDescription": "每 100 万 token 的价格。", @@ -131,7 +128,7 @@ export const dict = { "home.costSession": "成本/会话", "lab.title": "{{lab}} AI 模型使用量与排名 | OpenCode 数据", "lab.description": - "比较在 OpenCode Go 中使用的 {{lab}} 模型,包括 token 用量、模型排名、上下文窗口、发布日期、成本和模型专属数据。", + "比较在 OpenCode 中使用的 {{lab}} 模型,包括 token 用量、模型排名、上下文窗口、发布日期、成本和模型专属数据。", "lab.loadingTitle": "模型实验室", "lab.loadingDescription": "正在读取模型可用性和近期 OpenCode 使用情况。", "lab.notFound": "没有匹配此实验室的模型。", @@ -157,7 +154,7 @@ export const dict = { "lab.share": "份额", "model.title": "{{model}} 使用量、成本与排名 | OpenCode 数据", "model.description": - "查看 {{model}} 的 OpenCode Go 使用数据,包括 token 用量、周排名、token 组合、成本、缓存比例、会话、地理分布和同类模型。", + "查看 {{model}} 的 OpenCode 使用数据,包括 token 用量、周排名、token 组合、成本、缓存比例、会话、地理分布和同类模型。", "model.loadingTitle": "模型数据", "model.loadingDescription": "正在读取模型汇总。", "model.loadingProfile": "正在读取模型档案。", @@ -165,24 +162,24 @@ export const dict = { "model.noDataDescription": "请尝试从排行榜打开一个模型。", "model.noMatched": "没有匹配 {{id}} 的模型事实或使用行。", "model.fallback": "模型", - "model.catalogFallback": "来自共享模型索引的模型事实。此模型产生活动后会显示 OpenCode Go 使用量。", - "model.unranked": "未进入上周 OpenCode Go 使用量排名", - "model.ranked": "在上周 OpenCode Go 使用量中排名 #{{rank}}", + "model.catalogFallback": "来自共享模型索引的模型事实。此模型产生活动后会显示 OpenCode 使用量。", + "model.unranked": "未进入上周 OpenCode 使用量排名", + "model.ranked": "在上周 OpenCode 使用量中排名 #{{rank}}", "model.observedVolume": "占观测到的 2M 用量的 {{share}}。", "model.weights": "模型权重:{{label}}", "model.rank": "7 日排名", "model.profile": "模型档案", "model.listed": "已列出", - "model.noCurrentUsage": "当前数据窗口中没有 OpenCode Go 使用量。", + "model.noCurrentUsage": "当前数据窗口中没有 OpenCode 使用量。", "model.facts": "模型事实", "model.context": "上下文", "model.output": "输出", "model.knowledge": "知识", "model.release": "发布", "model.inputs": "输入", - "model.overviewDescription": "近期 OpenCode Go token、独立用户和市场位置。", + "model.overviewDescription": "近期 OpenCode token、独立用户和市场位置。", "model.noSummaryTitle": "无使用摘要", - "model.noSummaryDescription": "此模型尚无 OpenCode Go 使用行。", + "model.noSummaryDescription": "此模型尚无 OpenCode 使用行。", "model.tokens": "Token", "model.uniqueUsers": "独立用户", "model.lastTwoMonths": "过去两个月", @@ -192,15 +189,15 @@ export const dict = { "model.totalModels": "{{count}} 个模型", "model.momentum": "趋势", "model.vsPreviousWindow": "相较上一窗口", - "model.usageDescription": "近期两个月窗口内的每日 OpenCode Go token 用量。", + "model.usageDescription": "近期两个月窗口内的每日 OpenCode token 用量。", "model.noUsageTitle": "无使用量", "model.noUsageDescription": "当前窗口中没有使用量。", "model.dailyTokenChart": "每日 token 用量图", - "model.usersDescription": "近期两个月窗口内的每日 OpenCode Go 独立用户。", + "model.usersDescription": "近期两个月窗口内的每日 OpenCode 独立用户。", "model.noUsersTitle": "无用户数据", "model.noUsersDescription": "当前窗口中没有包含用户的行。", "model.dailyUserChart": "每日独立用户图", - "model.efficiencyDescription": "成本、缓存行为和平均 OpenCode Go 会话形态。", + "model.efficiencyDescription": "成本、缓存行为和平均 OpenCode 会话形态。", "model.noEfficiencyTitle": "无效率数据", "model.noEfficiencyDescription": "使用量到达后会显示效率数据。", "model.totalSpendLabel": "Total spend", @@ -218,11 +215,9 @@ export const dict = { "model.tokensSession": "Token / 会话", "model.cacheRatio": "缓存比例", "model.inputTokens": "输入 token", - "model.geoDescription": "按国家/地区统计的 OpenCode Go 模型 token 使用量。", "model.noGeoTitle": "无地理数据", - "model.noGeoDescription": "没有符合此模型的 OpenCode Go 地理行。", - "model.worldMap": "按国家/地区显示模型 token 使用量的世界地图", - "model.peersDescription": "按近期 OpenCode Go token 用量排列的相近模型。", + "model.noGeoDescription": "没有符合此模型的 OpenCode 地理行。", + "model.peersDescription": "按近期 OpenCode token 用量排列的相近模型。", "model.noPeersTitle": "无同类模型", "model.noPeersDescription": "使用量到达后会显示同类模型排名。", "model.noUsageLastWeek": "上周无使用量", diff --git a/packages/stats/app/src/i18n/zht.ts b/packages/stats/app/src/i18n/zht.ts index dca9ade4db..9f60343672 100644 --- a/packages/stats/app/src/i18n/zht.ts +++ b/packages/stats/app/src/i18n/zht.ts @@ -3,7 +3,7 @@ import type { Key } from "../i18n" export const dict = { "app.title": "AI 模型使用排名 | OpenCode 數據", "app.description": - "探索 OpenCode Go 在各 AI 模型上的使用情況,包括 token 用量、排名、市場佔有率、token 價格、工作階段成本、快取比例和地理分布。", + "探索 OpenCode 在各 AI 模型上的使用情況,包括 token 用量、排名、市場佔有率、token 價格、工作階段成本、快取比例和地理分布。", "app.unfurlAlt": "深色紋理背景上的 OpenCode Data 字標", "nav.dataHome": "數據首頁", "nav.topModels": "熱門模型", @@ -87,7 +87,7 @@ export const dict = { "home.loadingTitle": "正在載入數據", "home.loadingDescription": "正在讀取模型彙總。", "home.usageTitle": "用量", - "home.topModelsDescription": "OpenCode Go 中各模型的使用情況。", + "home.topModelsDescription": "OpenCode 中各模型的使用情況。", "home.noUsageTitle": "無使用數據", "home.noUsageDescription": "沒有符合該產品和時間範圍的模型列。", "home.noLeaderboardTitle": "無排行榜數據", @@ -99,7 +99,7 @@ export const dict = { "home.modelUsers": "模型使用者", "home.new": "新", "home.uniqueUsersTitle": "不重複使用者", - "home.uniqueUsersDescription": "按模型顯示的每日 OpenCode Go 不重複使用者。", + "home.uniqueUsersDescription": "按模型顯示的每日 OpenCode 不重複使用者。", "home.noUserDataTitle": "無使用者數據", "home.noUserDataDescription": "沒有符合該視窗且包含使用者的模型列。", "home.uniqueUsersChart": "按模型顯示的不重複使用者堆疊圖", @@ -110,11 +110,8 @@ export const dict = { "home.marketChart": "按模型作者顯示的市場佔有率", "home.noData": "無數據", "home.geoTitle": "地理分布", - "home.geoDescription": "按國家/地區統計的 token 使用量。", "home.noGeoTitle": "無地理數據", "home.noGeoDescription": "沒有符合該時間範圍的地理列。", - "home.worldMap": "按國家/地區顯示 token 使用量的世界地圖", - "home.geoMapTitle": "地理分布地圖", "home.unknown": "未知", "home.tokenCostTitle": "Token 成本", "home.tokenCostDescription": "每 100 萬 token 的價格。", @@ -131,7 +128,7 @@ export const dict = { "home.costSession": "成本/工作階段", "lab.title": "{{lab}} AI 模型使用量與排名 | OpenCode 數據", "lab.description": - "比較在 OpenCode Go 中使用的 {{lab}} 模型,包括 token 用量、模型排名、上下文視窗、發布日期、成本和模型專屬數據。", + "比較在 OpenCode 中使用的 {{lab}} 模型,包括 token 用量、模型排名、上下文視窗、發布日期、成本和模型專屬數據。", "lab.loadingTitle": "模型實驗室", "lab.loadingDescription": "正在讀取模型可用性和近期 OpenCode 使用情況。", "lab.notFound": "沒有符合此實驗室的模型。", @@ -157,7 +154,7 @@ export const dict = { "lab.share": "佔比", "model.title": "{{model}} 使用量、成本與排名 | OpenCode 數據", "model.description": - "查看 {{model}} 的 OpenCode Go 使用數據,包括 token 用量、週排名、token 組合、成本、快取比例、工作階段、地理分布和同類模型。", + "查看 {{model}} 的 OpenCode 使用數據,包括 token 用量、週排名、token 組合、成本、快取比例、工作階段、地理分布和同類模型。", "model.loadingTitle": "模型數據", "model.loadingDescription": "正在讀取模型彙總。", "model.loadingProfile": "正在讀取模型檔案。", @@ -165,24 +162,24 @@ export const dict = { "model.noDataDescription": "請嘗試從排行榜開啟一個模型。", "model.noMatched": "沒有符合 {{id}} 的模型事實或使用列。", "model.fallback": "模型", - "model.catalogFallback": "來自共享模型索引的模型事實。此模型產生活動後會顯示 OpenCode Go 使用量。", - "model.unranked": "未進入上週 OpenCode Go 使用量排名", - "model.ranked": "在上週 OpenCode Go 使用量中排名 #{{rank}}", + "model.catalogFallback": "來自共享模型索引的模型事實。此模型產生活動後會顯示 OpenCode 使用量。", + "model.unranked": "未進入上週 OpenCode 使用量排名", + "model.ranked": "在上週 OpenCode 使用量中排名 #{{rank}}", "model.observedVolume": "佔觀測到的 2M 用量的 {{share}}。", "model.weights": "模型權重:{{label}}", "model.rank": "7 日排名", "model.profile": "模型檔案", "model.listed": "已列出", - "model.noCurrentUsage": "目前數據視窗中沒有 OpenCode Go 使用量。", + "model.noCurrentUsage": "目前數據視窗中沒有 OpenCode 使用量。", "model.facts": "模型事實", "model.context": "上下文", "model.output": "輸出", "model.knowledge": "知識", "model.release": "發布", "model.inputs": "輸入", - "model.overviewDescription": "近期 OpenCode Go token、不重複使用者和市場位置。", + "model.overviewDescription": "近期 OpenCode token、不重複使用者和市場位置。", "model.noSummaryTitle": "無使用摘要", - "model.noSummaryDescription": "此模型尚無 OpenCode Go 使用列。", + "model.noSummaryDescription": "此模型尚無 OpenCode 使用列。", "model.tokens": "Token", "model.uniqueUsers": "不重複使用者", "model.lastTwoMonths": "過去兩個月", @@ -192,15 +189,15 @@ export const dict = { "model.totalModels": "{{count}} 個模型", "model.momentum": "趨勢", "model.vsPreviousWindow": "相較上一視窗", - "model.usageDescription": "近期兩個月視窗內的每日 OpenCode Go token 用量。", + "model.usageDescription": "近期兩個月視窗內的每日 OpenCode token 用量。", "model.noUsageTitle": "無使用量", "model.noUsageDescription": "目前視窗中沒有使用量。", "model.dailyTokenChart": "每日 token 用量圖", - "model.usersDescription": "近期兩個月視窗內的每日 OpenCode Go 不重複使用者。", + "model.usersDescription": "近期兩個月視窗內的每日 OpenCode 不重複使用者。", "model.noUsersTitle": "無使用者數據", "model.noUsersDescription": "目前視窗中沒有包含使用者的列。", "model.dailyUserChart": "每日不重複使用者圖", - "model.efficiencyDescription": "成本、快取行為和平均 OpenCode Go 工作階段形態。", + "model.efficiencyDescription": "成本、快取行為和平均 OpenCode 工作階段形態。", "model.noEfficiencyTitle": "無效率數據", "model.noEfficiencyDescription": "使用量到達後會顯示效率數據。", "model.totalSpendLabel": "Total spend", @@ -218,11 +215,9 @@ export const dict = { "model.tokensSession": "Token / 工作階段", "model.cacheRatio": "快取比例", "model.inputTokens": "輸入 token", - "model.geoDescription": "按國家/地區統計的 OpenCode Go 模型 token 使用量。", "model.noGeoTitle": "無地理數據", - "model.noGeoDescription": "沒有符合此模型的 OpenCode Go 地理列。", - "model.worldMap": "按國家/地區顯示模型 token 使用量的世界地圖", - "model.peersDescription": "按近期 OpenCode Go token 用量排列的相近模型。", + "model.noGeoDescription": "沒有符合此模型的 OpenCode 地理列。", + "model.peersDescription": "按近期 OpenCode token 用量排列的相近模型。", "model.noPeersTitle": "無同類模型", "model.noPeersDescription": "使用量到達後會顯示同類模型排名。", "model.noUsageLastWeek": "上週無使用量", diff --git a/packages/stats/app/src/routes/[lab]/[model].tsx b/packages/stats/app/src/routes/[lab]/[model].tsx index fb6c19748a..6753afa3cb 100644 --- a/packages/stats/app/src/routes/[lab]/[model].tsx +++ b/packages/stats/app/src/routes/[lab]/[model].tsx @@ -9,6 +9,7 @@ import { type ModelUsagePoint, type StatsModelData, } from "@opencode-ai/stats-core/domain/home" +import { statModel } from "@opencode-ai/stats-core/domain/model-normalization" import { createAsync, query, useParams } from "@solidjs/router" import { createMemo, createSignal, createUniqueId, For, onMount, Show, type JSX } from "solid-js" import { getRequestEvent } from "solid-js/web" @@ -16,8 +17,13 @@ import { LocaleLinks } from "../../component/locale-links" import { useI18n } from "../../context/i18n" import { useLanguage } from "../../context/language" import { localizedUrl } from "../../lib/language" -import { findModelCatalogEntry, formatCatalogLabName, loadModelCatalog, type ModelCatalogEntry } from "../model-catalog" -import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "../geo-map" +import { + findModelCatalogEntry, + formatCatalogLabName, + isKnownCatalogLab, + loadModelCatalog, + type ModelCatalogEntry, +} from "../model-catalog" import { SectionHeading } from "../section-heading" import { runStatsEffect } from "../../stats-runtime" import { setStatsPageCacheHeaders } from "../stats-cache" @@ -41,6 +47,8 @@ import { } from "../stats-shell" const statsUnfurlPath = "banner.png" +const glmFlashCatalogId = "zhipuai/glm-5.3-flash" +const glmFlashModel = "glm-5.3-flash" const shortMonths = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const type IsoCountryCode = readonly [string, string, string] @@ -50,7 +58,7 @@ type ModelPageCatalog = { labs: { id: string; name: string }[] labModels: ModelCatalogOption[] } -type StatsModelPageData = Omit & { country: CountryEntry[] } +type StatsModelPageData = StatsModelData type ModelPageData = { catalog: ModelPageCatalog; stats: StatsModelPageData | null } const countryNumericIds = new Map( @@ -73,7 +81,7 @@ const getModelPageData = query(async (labParam: string, modelParam: string) => { .find((item) => item.id === (entry?.lab ?? providerSlug(labParam))) ?.models.map((item) => ({ id: item.id, lab: item.lab, slug: item.slug, name: item.name })) ?? [], }, - stats: stats ? { ...stats, country: stats.country["2M"] } : null, + stats, } satisfies ModelPageData }, "getStatsModelPageData") @@ -90,14 +98,23 @@ export default function StatsModel() { const stats = createMemo(() => page()?.stats) const githubStars = createAsync(() => getGitHubStars()) const [themePreference, setThemePreference] = createSignal("system") - const modelName = createMemo(() => catalogEntry()?.name ?? stats()?.model ?? modelParam() ?? i18n.t("model.fallback")) - const labName = createMemo(() => formatCatalogLabName(catalogEntry()?.lab ?? stats()?.provider ?? labParam())) - const modelTitle = createMemo(() => i18n.t("model.title", { model: modelName() })) - const modelDescription = createMemo(() => i18n.t("model.description", { model: modelName() })) - const modelPath = createMemo( - () => - `/data/${catalogEntry()?.id ?? [labParam(), stats()?.slug ?? modelParam()].filter((part) => part.length > 0).join("/")}`, + const canonicalModel = createMemo(() => statModel(stats()?.model ?? modelParam(), undefined)) + const modelName = createMemo( + () => catalogEntry()?.name ?? publicModelName(canonicalModel()) ?? i18n.t("model.fallback"), ) + const lab = createMemo(() => catalogEntry()?.lab ?? stats()?.provider ?? labParam()) + const catalogLabs = createMemo(() => page()?.catalog.labs.map((item) => item.id) ?? []) + const labName = createMemo(() => (isKnownCatalogLab(lab(), catalogLabs()) ? formatCatalogLabName(lab()) : undefined)) + const formerName = createMemo(() => formerModelName(canonicalModel())) + const searchModelName = createMemo(() => (formerName() ? `${modelName()} (formerly ${formerName()})` : modelName())) + const modelTitle = createMemo(() => i18n.t("model.title", { model: searchModelName() })) + const modelDescription = createMemo(() => i18n.t("model.description", { model: searchModelName() })) + const modelPath = createMemo(() => { + const fallback = formerName() + ? glmFlashCatalogId + : [labParam(), stats()?.slug ?? canonicalModel()].filter((part) => part.length > 0).join("/") + return `/data/${catalogEntry()?.id ?? fallback}` + }) const modelUrl = createMemo(() => localizedUrl(language.locale(), modelPath())) const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString() const modelHeaderLinks = createMemo(() => [ @@ -168,6 +185,7 @@ export default function StatsModel() { catalog={catalogEntry() ?? null} catalogData={page()?.catalog ?? null} labName={labName()} + formerName={formerName()} /> @@ -175,9 +193,14 @@ export default function StatsModel() { - + props.catalog?.lab ?? props.data?.provider ?? props.labName + const labId = () => props.catalog?.lab ?? props.data?.provider + const hasLab = () => props.labName !== undefined const modelName = () => props.catalog?.name ?? props.data?.model ?? i18n.t("model.fallback") const weights = () => props.catalog?.weights[0] const labs = () => props.catalogData?.labs ?? [] @@ -270,35 +295,28 @@ function ModelHero(props: { Data - / - 0} - fallback={ - - {props.labName} - - - } - > - ({ - href: language.route(`${import.meta.env.BASE_URL}${lab.id}`), - label: lab.name, - value: lab.id, - }))} - value={providerSlug(labId())} - variant="model" - /> + + / + 0} fallback={{props.labName}}> + ({ + href: language.route(`${import.meta.env.BASE_URL}${lab.id}`), + label: lab.name, + value: lab.id, + }))} + value={providerSlug(labId() ?? "")} + variant="model" + /> + / 0} fallback={ - - {modelName()} - + + {modelName()} } > @@ -317,9 +335,11 @@ function ModelHero(props: {
    - - + + + +

    {modelName()}

    @@ -337,6 +357,7 @@ function ModelHero(props: { when={props.data} fallback={

    + {(name) => {`Formerly ${name()}.`}} Listed across the shared model catalog.

    @@ -344,13 +365,14 @@ function ModelHero(props: { > {(data) => (

    + {(name) => {`Formerly ${name()}.`}} Ranked {formatHeroRank(data().rank)} across last week's - OpenCode Go + OpenCode usage with {formatPercent(data().tokenShare)} of observed @@ -471,6 +493,7 @@ function ModelMomentumSection(props: { data: StatsModelPageData | null }) { value={formatInteger(data().totals.sessions)} /> +

    - {props.rowLabel} + + {props.rowLabel} {props.formatValue(props.value(active.point))}

    @@ -891,21 +920,10 @@ function ModelEfficiencySection(props: { data: StatsModelPageData | null; catalo function ModelGeoBreakdownSection(props: { data: CountryEntry[] }) { const i18n = useI18n() - const language = useLanguage() const [activeCountry, setActiveCountry] = createSignal() const data = createMemo(() => props.data) - const countryById = createMemo( - () => - new Map( - data().flatMap((country) => { - const id = countryNumericId(country.country) - return id ? [[id, country] as const] : [] - }), - ), - ) const maxTokens = createMemo(() => Math.max(0, ...data().map((country) => country.tokens)) || 1) const topCountries = createMemo(() => data().slice(0, 15)) - const active = createMemo(() => data().find((country) => country.country === activeCountry()) ?? data()[0]) return (
    - + 0} fallback={} >
    -
    - - - {(country) => ( -
    - #{String(country().rank).padStart(2, "0")} - {formatCountryName(country().country, language.tag(language.locale()), i18n)} -

    - {formatGeoTokens(country().tokens)} - {formatGeoShare(country().share)} -

    -
    - )} -
    -
    - activeCountry: string | undefined - maxTokens: number - onActiveCountryChange: (country: string | undefined) => void -}) { - const i18n = useI18n() - const opacityScale = createMemo(() => scaleSqrt().domain([0, props.maxTokens]).range([0.26, 0.96]).clamp(true)) - const countryOpacity = (country: CountryEntry | undefined) => { - if (!country || country.tokens <= 0) return 0 - const opacity = opacityScale()(country.tokens) - if (props.activeCountry === country.country) return 1 - if (!props.activeCountry) return opacity - return Math.max(0.18, opacity * 0.36) - } - - return ( - - {i18n.t("home.geoMapTitle")} - - - {(country) => { - const entry = () => props.countryById.get(country.id) - return ( - - - - - {(country) => { - const entry = () => props.countryById.get(country.id) - return ( - - - ) - }} - - - - - ) -} - function GeoCountryList(props: { data: CountryEntry[] activeCountry: string | undefined @@ -1081,7 +989,7 @@ function GeoCountryList(props: { ) } -function ModelPeersSection(props: { data: StatsModelPageData | null }) { +function ModelPeersSection(props: { data: StatsModelPageData | null; catalogLabs: readonly string[] }) { const i18n = useI18n() return (
    @@ -1094,7 +1002,9 @@ function ModelPeersSection(props: { data: StatsModelPageData | null }) { >
      - {(peer) => } + {(peer) => ( + + )}
    @@ -1112,23 +1022,29 @@ function MetricCard(props: { label: string; value: string; detail?: string; stat ) } -function PeerRow(props: { peer: ModelPeerEntry; active: boolean }) { +function PeerRow(props: { peer: ModelPeerEntry; active: boolean; catalogLabs: readonly string[] }) { const language = useLanguage() + const hasProvider = () => isKnownCatalogLab(props.peer.provider, props.catalogLabs) return (
  • {String(props.peer.rank).padStart(2, "0")} - - + + + + {props.peer.model} - {props.peer.author} + + {props.peer.author} + {formatTokens(props.peer.tokens)} @@ -1136,7 +1052,7 @@ function PeerRow(props: { peer: ModelPeerEntry; active: boolean }) { ) } -function SectionTitle(props: { href: string; title: string; description: string }) { +function SectionTitle(props: { href: string; title: string; description?: string }) { return } @@ -1151,6 +1067,7 @@ function ModelEmptyState(props: { title: string; description: string; compact?: function modelComparisonPairs( catalogModels: ModelCatalogOption[] | undefined, + catalogLabs: readonly string[], catalogEntry: ModelCatalogEntry | null, data: StatsModelPageData | null, ) { @@ -1165,7 +1082,7 @@ function modelComparisonPairs( name: peer.model, lab: peer.provider, slug: peer.slug, - labName: peer.author, + labName: isKnownCatalogLab(peer.provider, catalogLabs) ? peer.author : undefined, metric: `#${peer.rank} / ${formatTokens(peer.tokens)}`, }, detail: "Usage peer", @@ -1191,7 +1108,7 @@ function modelComparisonRef( name: data.model, lab: data.provider, slug: data.slug, - labName: data.author, + labName: undefined, metric: `#${data.rank}`, } } @@ -1553,3 +1470,13 @@ function providerSlug(provider: string) { .replace(/^-+|-+$/g, "") .replace(/-{2,}/g, "-") } + +function formerModelName(model: string) { + return statModel(model, undefined) === glmFlashModel ? "ox-alpha" : undefined +} + +function publicModelName(model: string) { + if (model === "unknown") return undefined + if (model === glmFlashModel) return "GLM-5.3-Flash" + return model +} diff --git a/packages/stats/app/src/routes/[lab]/index.tsx b/packages/stats/app/src/routes/[lab]/index.tsx index ab202fc89f..f79f5c21f4 100644 --- a/packages/stats/app/src/routes/[lab]/index.tsx +++ b/packages/stats/app/src/routes/[lab]/index.tsx @@ -473,13 +473,15 @@ function LabUsageSection(props: { lab: ModelCatalogLab; data: StatsLabData | nul

    - {i18n.t("lab.dailyTokens")} + + {i18n.t("lab.dailyTokens")} {formatTokens(active.point.tokens)}

    - {i18n.t("model.uniqueUsers")} + + {i18n.t("model.uniqueUsers")} {formatUsers(active.point.users)}

    @@ -651,7 +653,7 @@ function LabModelTooltip(props: { state: LabModelTooltipState }) { {props.state.model.name}
    -

    {props.state.model.description ?? "Recent OpenCode Go usage, share, context, and output limits."}

    +

    {props.state.model.description ?? "Recent OpenCode usage, share, context, and output limits."}

  • diff --git a/packages/stats/app/src/routes/compare-cards.tsx b/packages/stats/app/src/routes/compare-cards.tsx index 24077bc3b9..7861f9b32f 100644 --- a/packages/stats/app/src/routes/compare-cards.tsx +++ b/packages/stats/app/src/routes/compare-cards.tsx @@ -119,17 +119,24 @@ function ComparisonCardIcon() { } function ComparisonPanelCard(props: { pair: ComparisonPair }) { + const firstLabName = () => props.pair.first.labName + const secondLabName = () => props.pair.second.labName + return ( {props.pair.detail} {props.pair.first.name} vs {props.pair.second.name} -

    - {props.pair.first.labName ?? formatCatalogLabName(props.pair.first.lab)} - - {props.pair.second.labName ?? formatCatalogLabName(props.pair.second.lab)} -

    + +

    + {(name) => {name()}} + + + + {(name) => {name()}} +

    +
    {props.pair.first.metric ?? "Listed"} / {props.pair.second.metric ?? "Listed"} @@ -143,14 +150,11 @@ function ComparisonLabLogo(props: { model: ComparisonModelRef }) { const iconId = () => providerIconId(props.model.lab) return ( - - + + + + ) } diff --git a/packages/stats/app/src/routes/compare-radar.tsx b/packages/stats/app/src/routes/compare-radar.tsx index d7403809ac..35ada12885 100644 --- a/packages/stats/app/src/routes/compare-radar.tsx +++ b/packages/stats/app/src/routes/compare-radar.tsx @@ -9,7 +9,7 @@ const toolUseBenchmarkPattern = /(terminal bench|claw eval|tau ?(?:bench|2|3))/ export type ComparisonRadarModel = { name: string - labName: string + labName?: string catalog: ModelCatalogEntry | null } @@ -61,7 +61,7 @@ export function ComparisonRadar(props: ComparisonRadarProps) {