mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-24 17:41:37 +08:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49800a00bd | |||
| f9dcd97936 | |||
| d763c11a6d | |||
| b1202ac6db | |||
| d469d7d441 | |||
| 48dc520fb8 | |||
| 668d5a76d5 | |||
| b9c1f10016 | |||
| 8a0c86cbdb | |||
| 7f86fe3f61 | |||
| a32cf70d7e | |||
| a607f33552 | |||
| 350a32274a | |||
| 27c99b46cb | |||
| 1d6e3d477b | |||
| efbb973393 | |||
| 45bc7a6a9d | |||
| 088ebb967f | |||
| bcf740f98a | |||
| 6b80fff2bb | |||
| 2e63fedb76 | |||
| 5a9f4e5c60 | |||
| d0a48a09e2 | |||
| c0a21e7025 | |||
| 10cc15aabe | |||
| adf7681100 | |||
| 0237905b96 | |||
| e8aa79bab6 | |||
| 4ff5783e59 | |||
| dcfeb52983 | |||
| 46790e57e9 | |||
| 4bc3fa0826 | |||
| 88cfb979be | |||
| 32b5db754e | |||
| f33f8ca109 | |||
| 598d63db63 | |||
| 38bff1b372 | |||
| e8c9b21f20 | |||
| 6d3fc63658 | |||
| ee4437ff32 | |||
| 7a4aa68706 | |||
| f00380d285 | |||
| c00d4885c6 | |||
| 70f4722356 | |||
| 8898bf7ca4 | |||
| e5b13b767e | |||
| 3181c68cbb | |||
| c3c9003dbb | |||
| 921b98066d | |||
| c5b4cc80cc | |||
| 0bccd1d578 | |||
| 91db82c138 | |||
| 0eb97086fc | |||
| 5b34636afa | |||
| f1138b9a55 | |||
| 23ff6dbba4 | |||
| b457923970 | |||
| 66e4a5a64e | |||
| 6c25e64658 | |||
| f2fd0f8f00 | |||
| 44cdde5422 | |||
| 88235dc618 | |||
| 4d2b671d7b | |||
| 8098031eac | |||
| 74c882d9d0 | |||
| 80636fec43 | |||
| a8ad74aef3 | |||
| e2e2b7934e | |||
| 28c802f399 | |||
| bcfa63aa4e | |||
| d40feafb01 | |||
| 2a8473891b | |||
| a4e3451d5c | |||
| 53a7c2885b | |||
| f354507d42 | |||
| f17e1def32 | |||
| 3183e8b7d4 | |||
| 9d2b9ef2d4 | |||
| 733e5cd876 | |||
| 4ee4f7bcb3 | |||
| da7ecda9ea | |||
| 1f11d4fb1a | |||
| b308503ab5 | |||
| 3b10de4a28 | |||
| 6ce1de476a | |||
| d9b0848a61 | |||
| 46dd3b8166 | |||
| eca07be072 | |||
| 165d57b88e | |||
| 28c44f7e5a | |||
| 39d5bdff4b | |||
| b9f8480b2f | |||
| 58b30d678a | |||
| 408cdaf5e0 | |||
| cae23cde09 | |||
| 702fb2562c | |||
| 785d0b60b6 | |||
| 67ab9dc4d0 | |||
| 77494cb7df | |||
| 08f8856d6c | |||
| 79a4c1544d | |||
| c0a35141e6 | |||
| 221bb64aeb | |||
| 2555dba188 | |||
| 6355ed6ae7 | |||
| 1864e8c863 | |||
| 61d0d66dae | |||
| 86522f1b3e | |||
| dc32705bc9 | |||
| 1eaf5c31d3 | |||
| 677b19e22e | |||
| 8e248ae045 |
@@ -1,57 +0,0 @@
|
||||
#
|
||||
# This file is intentionally in the wrong dir, will move and add later....
|
||||
#
|
||||
|
||||
name: Guidelines Check
|
||||
|
||||
on:
|
||||
# Disabled - uncomment to re-enable
|
||||
# pull_request_target:
|
||||
# types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-guidelines:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Check PR guidelines compliance
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
|
||||
run: |
|
||||
opencode run -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
|
||||
|
||||
<pr-number>
|
||||
${{ github.event.pull_request.number }}
|
||||
</pr-number>
|
||||
|
||||
<pr-description>
|
||||
${{ github.event.pull_request.body }}
|
||||
</pr-description>
|
||||
|
||||
Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
|
||||
|
||||
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
|
||||
|
||||
Command MUST be like this.
|
||||
```
|
||||
gh api \
|
||||
--method POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
|
||||
-f 'body=[summary of issue]' -f 'commit_id=${{ github.event.pull_request.head.sha }}' -f 'path=[path-to-file]' -F "line=[line]" -f 'side=RIGHT'
|
||||
```
|
||||
|
||||
Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Guidelines Check
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
check-guidelines:
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
startsWith(github.event.comment.body, '/review') &&
|
||||
contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Get PR number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Get PR details
|
||||
id: pr-details
|
||||
run: |
|
||||
gh api /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }} > pr_data.json
|
||||
echo "title=$(jq -r .title pr_data.json)" >> $GITHUB_OUTPUT
|
||||
echo "sha=$(jq -r .head.sha pr_data.json)" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check PR guidelines compliance
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
|
||||
run: |
|
||||
PR_BODY=$(jq -r .body pr_data.json)
|
||||
opencode run -m anthropic/claude-sonnet-4-5 "A new pull request has been created: '${{ steps.pr-details.outputs.title }}'
|
||||
|
||||
<pr-number>
|
||||
${{ steps.pr-number.outputs.number }}
|
||||
</pr-number>
|
||||
|
||||
<pr-description>
|
||||
$PR_BODY
|
||||
</pr-description>
|
||||
|
||||
Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
|
||||
|
||||
When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.
|
||||
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
|
||||
|
||||
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
|
||||
|
||||
Command MUST be like this.
|
||||
\`\`\`
|
||||
gh api \
|
||||
--method POST \
|
||||
-H \"Accept: application/vnd.github+json\" \
|
||||
-H \"X-GitHub-Api-Version: 2022-11-28\" \
|
||||
/repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }}/comments \
|
||||
-f 'body=[summary of issue]' -f 'commit_id=${{ steps.pr-details.outputs.sha }}' -f 'path=[path-to-file]' -F \"line=[line]\" -f 'side=RIGHT'
|
||||
\`\`\`
|
||||
|
||||
Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Git commit and push
|
||||
description: git commit and push
|
||||
---
|
||||
|
||||
commit and push
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: "Find issue(s) on github"
|
||||
description: "find issue(s) on github"
|
||||
model: opencode/claude-haiku-4-5
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Spellcheck all markdown file changes
|
||||
description: spellcheck all markdown file changes
|
||||
---
|
||||
|
||||
Look at all the unstaged changes to markdown (.md, .mdx) files, pull out the lines that have changed, and check for spelling and grammar errors.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["opencode-openai-codex-auth"],
|
||||
"enterprise": {
|
||||
"url": "https://enterprise.dev.opencode.ai",
|
||||
},
|
||||
// "enterprise": {
|
||||
// "url": "https://enterprise.dev.opencode.ai",
|
||||
// },
|
||||
"instructions": ["STYLE_GUIDE.md"],
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
sst-env.d.ts
|
||||
@@ -1,16 +1,3 @@
|
||||
## IMPORTANT
|
||||
|
||||
- Try to keep things in one function unless composable or reusable
|
||||
- DO NOT do unnecessary destructuring of variables
|
||||
- DO NOT use `else` statements unless necessary
|
||||
- DO NOT use `try`/`catch` if it can be avoided
|
||||
- AVOID `try`/`catch` where possible
|
||||
- AVOID `else` statements
|
||||
- AVOID using `any` type
|
||||
- AVOID `let` statements
|
||||
- PREFER single word variable names where possible
|
||||
- Use as many bun apis as possible like Bun.file()
|
||||
|
||||
## Debugging
|
||||
|
||||
- To test opencode in the `packages/opencode` directory you can run `bun dev`
|
||||
|
||||
@@ -42,6 +42,8 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
|
||||
> [!NOTE]
|
||||
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
|
||||
|
||||
Please try to follow the [style guide](./STYLE_GUIDE.md)
|
||||
|
||||
### Setting up a Debugger
|
||||
|
||||
Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points.
|
||||
|
||||
@@ -157,3 +157,6 @@
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
## Style Guide
|
||||
|
||||
- Try to keep things in one function unless composable or reusable
|
||||
- DO NOT do unnecessary destructuring of variables
|
||||
- DO NOT use `else` statements unless necessary
|
||||
- DO NOT use `try`/`catch` if it can be avoided
|
||||
- AVOID `try`/`catch` where possible
|
||||
- AVOID `else` statements
|
||||
- AVOID using `any` type
|
||||
- AVOID `let` statements
|
||||
- PREFER single word variable names where possible
|
||||
- Use as many bun apis as possible like Bun.file()
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -75,7 +75,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -99,7 +99,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -123,7 +123,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -164,7 +164,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -192,7 +192,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
@@ -208,7 +208,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -238,8 +238,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.2.8",
|
||||
"@opentui/core": "0.1.54",
|
||||
"@opentui/solid": "0.1.54",
|
||||
"@opentui/core": "0.1.56",
|
||||
"@opentui/solid": "0.1.56",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -297,7 +297,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -317,7 +317,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -328,7 +328,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -341,7 +341,7 @@
|
||||
},
|
||||
"packages/tauri": {
|
||||
"name": "@opencode-ai/tauri",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
@@ -354,7 +354,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -386,7 +386,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -397,7 +397,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -443,7 +443,7 @@
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/precision-diffs": "0.5.7",
|
||||
"@pierre/precision-diffs": "0.6.0-beta.3",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
@@ -1088,21 +1088,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.54", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.54", "@opentui/core-darwin-x64": "0.1.54", "@opentui/core-linux-arm64": "0.1.54", "@opentui/core-linux-x64": "0.1.54", "@opentui/core-win32-arm64": "0.1.54", "@opentui/core-win32-x64": "0.1.54", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-NYBVOmAa3JB+bSTxFTUc3Ej3B8Gc364DMsoktVRCYdjQ+AyeMRNdOTj9sMWdDJbXqITK/atrsUXouoE5bL9sWA=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.56", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.56", "@opentui/core-darwin-x64": "0.1.56", "@opentui/core-linux-arm64": "0.1.56", "@opentui/core-linux-x64": "0.1.56", "@opentui/core-win32-arm64": "0.1.56", "@opentui/core-win32-x64": "0.1.56", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-TI5cSCPYythHIQYpAEdXyZhewGACn2TfnfC1qZmrSyEq33zFo4W7zpQ4EZNpy9xZJFCI+elAUVJFARwhudp9EQ=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.54", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LyJ2AI9XA3upckiXLaX/P6tB40pB6euZA5MCikyhwcDVew3Z7NpTI0xHp4gUWHwsSTwmx6u851ofgkf7nMBEJg=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.56", "", { "os": "darwin", "cpu": "arm64" }, "sha512-x5U9J2k1Fmbb9Mdh1nOd/yZVpg4ARCrV5pFngpaeKrIWDhs8RLpQW3ap+r7uyFLGFkSn4h5wBR0jj6Dg+Tyw+A=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.54", "", { "os": "darwin", "cpu": "x64" }, "sha512-LZAjXR1OoUr8pewBTHqxupdPS4PC4qQCht0HHGEr1+zN7zu5c+kcp3Uop+NeQTYLjHJftiULukzAfLJMXFuDNA=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.56", "", { "os": "darwin", "cpu": "x64" }, "sha512-7swq9rV/SaNVBWoUbC7mlP1VNyKBl7SSwmyVMkcaBP71lkm95zWuh4pgGj82fLgZ9gITRBD95TJVDmTovOyW0A=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.54", "", { "os": "linux", "cpu": "arm64" }, "sha512-AC7qBYc4shY28/eR1BvguvfCjLzZJy0mdvadSdDF0XJJIe5vhWJ9UyKsoyLo7pwrd0iFBf5d7fg4kJcbLbZNWg=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.56", "", { "os": "linux", "cpu": "arm64" }, "sha512-v8b+kiTlynAJzR0hFeVpGFzVi5PGqXAe3Zql9iTiQqTExkm/sR34sfC/P6rBOUhuAnos8ovPDKWtDb6eCTSm9g=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.54", "", { "os": "linux", "cpu": "x64" }, "sha512-S7BAq4CUV6ZfoXDSClJT75XxVSKPOOuRVYvBSHMOgwkBwLHpJZdz0Z4mzcGBBP1nJQ5Gz92TWPYfYMbZvR1lpw=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.56", "", { "os": "linux", "cpu": "x64" }, "sha512-lbxgvAi5SBswK/2hoMPtLhPvJxASgquPUwvGTRHqzDkCvrOChP/loTjBQpL09/nAFc3jbM3SAbZtnEgA2SGYVw=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.54", "", { "os": "win32", "cpu": "arm64" }, "sha512-K+E9i8t6YfN0Ly9moHefRQfR5GSbQUAsqzfrW4TD4bpJBy5y7EKh1mz8ZdES/RwWOSGUCb+JN7/ZQm0OubbdvQ=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.56", "", { "os": "win32", "cpu": "arm64" }, "sha512-RoCAbvDo+59OevX+6GrEGbaueERiBVnTaWJkrS41hRAD2fFS3CZpW7UuS5jIg7zn5clHmOGyfvCiBkTRXmgkhw=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.54", "", { "os": "win32", "cpu": "x64" }, "sha512-0KsBPRtcqmPM1AoN2Ez9au6uf7y8631W9tXTWnVZRlI2/G3j1AfGReR6vuwrd8O4JfxFXiYWWPfSxYZvWLSRNg=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.56", "", { "os": "win32", "cpu": "x64" }, "sha512-i6N5TjZU5gRkJsKmH8e/qY9vwSk0rh6A5t37mHDGlzN4E5yO/MbBrYH4ppLp5stps9Zfi1Re51ofJX1s2hZY/Q=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.54", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.54", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-rr9moI+W3meoD57t4Flyfw33mRSFIX0FOx+t4T3sLNA3Pz5FGU2n4qyxPc21OkAuc23S+YxuEuwbZebnIaLGWA=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.56", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.56", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-3R7AfxsYHUyehwJK98rt5dI9u2WCT/uH/CYvddZIgXPHyfFm1SHJekMdy3DUoiQTCUllt68eFGKMv9zRi6Laww=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -1218,7 +1218,7 @@
|
||||
|
||||
"@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
|
||||
|
||||
"@pierre/precision-diffs": ["@pierre/precision-diffs@0.5.7", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-Y+e4kJ9pT2I4NS5fE39KdoiXtwMkVPRvrwLM6O2IqO7PDCRWLBS7CYxcSgSyngEndccUll2krx66I2QnfO0Ovg=="],
|
||||
"@pierre/precision-diffs": ["@pierre/precision-diffs@0.6.0-beta.3", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-1FBm9jhLWZvs7BqN3yG2Wh9SpGuO1us2QsKZlQqSwyCctMr9DRGzYQJ9lF6yR03LHzXs3fuIzO++d9sCObYzrQ=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1764587062,
|
||||
"narHash": "sha256-hdFa0TAVQAQLDF31cEW3enWmBP+b592OvHs6WVe3D8k=",
|
||||
"lastModified": 1764733908,
|
||||
"narHash": "sha256-QJiih52NU+nm7XQWCj+K8SwUdIEayDQ1FQgjkYISt4I=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c1cb7d097cb250f6e1904aacd5f2ba5ffd8a49ce",
|
||||
"rev": "cadcc8de247676e4751c9d4a935acb2c0b059113",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -13,6 +13,10 @@ inputs:
|
||||
description: "Share the opencode session (defaults to true for public repos)"
|
||||
required: false
|
||||
|
||||
prompt:
|
||||
description: "Custom prompt to override the default prompt"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -27,3 +31,4 @@ runs:
|
||||
env:
|
||||
MODEL: ${{ inputs.model }}
|
||||
SHARE: ${{ inputs.share }}
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
|
||||
Vendored
+1
-1
@@ -6,4 +6,4 @@
|
||||
/// <reference path="../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
+1
-1
@@ -116,7 +116,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||
// CONSOLE
|
||||
////////////////
|
||||
|
||||
const bucket = new sst.cloudflare.Bucket("ConsoleData")
|
||||
const bucket = new sst.cloudflare.Bucket("ZenData")
|
||||
|
||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||
|
||||
@@ -4,7 +4,7 @@ APP=opencode
|
||||
|
||||
MUTED='\033[0;2m'
|
||||
RED='\033[0;31m'
|
||||
ORANGE='\033[38;2;255;140;0m'
|
||||
ORANGE='\033[38;5;214m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
requested_version=${VERSION:-}
|
||||
@@ -353,10 +353,9 @@ echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░
|
||||
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
|
||||
echo -e ""
|
||||
echo -e ""
|
||||
echo -e "${MUTED}To get started, navigate to a project and run:${NC}"
|
||||
echo -e "opencode ${MUTED}Use free models${NC}"
|
||||
echo -e "opencode auth login ${MUTED}Add paid provider API keys${NC}"
|
||||
echo -e "opencode help ${MUTED}List commands and options${NC}"
|
||||
echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
|
||||
echo -e "cd <project> ${MUTED}# Open directory${NC}"
|
||||
echo -e "opencode ${MUTED}# Run command${NC}"
|
||||
echo -e ""
|
||||
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
|
||||
echo -e ""
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-9BfJ3dFq/UYyhsnK3Sfx6rb6CT8bCvFOFOqD2+W1WQE="
|
||||
"nodeModules": "sha256-ZGKC7h4ScHDzVYj8qb1lN/weZhyZivPS8kpNAZvgO0I="
|
||||
}
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/precision-diffs": "0.5.7",
|
||||
"@pierre/precision-diffs": "0.6.0-beta.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"ai": "5.0.97",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/sst/opencode",
|
||||
starsFormatted: {
|
||||
compact: "30K",
|
||||
full: "30,000",
|
||||
compact: "35K",
|
||||
full: "35,000",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ export const config = {
|
||||
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "300",
|
||||
commits: "4,000",
|
||||
monthlyUsers: "300,000",
|
||||
contributors: "350",
|
||||
commits: "5,000",
|
||||
monthlyUsers: "400,000",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -338,6 +338,11 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="installation-instructions"] {
|
||||
color: var(--color-text-strong);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
[data-slot="installation"] {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
@@ -348,6 +353,11 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="installation-options"] {
|
||||
font-size: 13px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
[data-component="tabs"] {
|
||||
[data-slot="tablist"] {
|
||||
display: flex;
|
||||
@@ -480,10 +490,10 @@ body {
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-size: 38px;
|
||||
color: var(--color-text-strong);
|
||||
font-weight: 500;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
@media (max-width: 60rem) {
|
||||
font-size: 22px;
|
||||
@@ -492,7 +502,7 @@ body {
|
||||
|
||||
p {
|
||||
color: var(--color-text);
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 40px;
|
||||
max-width: 82%;
|
||||
|
||||
@media (max-width: 50rem) {
|
||||
@@ -607,6 +617,25 @@ body {
|
||||
padding: var(--vertical-padding) var(--padding);
|
||||
color: var(--color-text);
|
||||
|
||||
a {
|
||||
background: var(--color-background-strong);
|
||||
padding: 8px 12px 8px 20px;
|
||||
color: var(--color-text-inverted);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
gap: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background: var(--color-background-strong-hover);
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
li {
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function Home() {
|
||||
return (
|
||||
<main data-page="opencode">
|
||||
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
|
||||
<Title>OpenCode | The AI coding agent built for the terminal</Title>
|
||||
<Title>OpenCode | The open source AI coding agent</Title>
|
||||
<Link rel="canonical" href={config.baseUrl} />
|
||||
<Meta property="og:image" content="/social-share.png" />
|
||||
<Meta name="twitter:image" content="/social-share.png" />
|
||||
@@ -56,23 +56,13 @@ export default function Home() {
|
||||
<a data-slot="releases" href={release()?.url ?? `${config.github.repoUrl}/releases`} target="_blank">
|
||||
What’s new in {release()?.name ?? "the latest release"}
|
||||
</a>
|
||||
<h1>The AI coding agent built for the terminal</h1>
|
||||
<h1>The open source coding agent</h1>
|
||||
<p>
|
||||
OpenCode is fully open source, giving you control and freedom to use any provider, any model, and any
|
||||
editor.
|
||||
OpenCode includes free models or connect from any provider to <br />
|
||||
use other models, including Claude, GPT, Gemini and more.
|
||||
</p>
|
||||
<a href="/docs">
|
||||
<span>Read docs </span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<p data-slot="installation-instructions">Install and use. No account, no email, and no credit card.</p>
|
||||
<div data-slot="installation">
|
||||
<Tabs
|
||||
as="section"
|
||||
@@ -151,6 +141,11 @@ export default function Home() {
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
<p data-slot="installation-options">
|
||||
Available in terminal, web, and desktop (coming soon).
|
||||
<br />
|
||||
Extensions for VS Code, Cursor, Windsurf, and more.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section data-component="video">
|
||||
@@ -208,6 +203,17 @@ export default function Home() {
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/docs">
|
||||
<span>Read docs </span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section data-component="growth">
|
||||
|
||||
@@ -14,7 +14,7 @@ import "./workspace-picker.css"
|
||||
const getWorkspaces = query(async () => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
return Database.transaction((tx) =>
|
||||
return Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
id: WorkspaceTable.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createEffect, createMemo, onCleanup, Show, For } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
@@ -94,8 +94,6 @@ async function getCosts(workspaceID: string, year: number, month: number) {
|
||||
}, workspaceID)
|
||||
}
|
||||
|
||||
const queryCosts = query(getCosts, "costs.get")
|
||||
|
||||
const MODEL_COLORS: Record<string, string> = {
|
||||
"claude-sonnet-4-5": "#D4745C",
|
||||
"claude-sonnet-4": "#E8B4A4",
|
||||
@@ -158,32 +156,27 @@ export function GraphSection() {
|
||||
model: null as string | null,
|
||||
modelDropdownOpen: false,
|
||||
keyDropdownOpen: false,
|
||||
colorScheme: "light" as "light" | "dark",
|
||||
})
|
||||
const initialData = createAsync(() => queryCosts(params.id!, store.year, store.month))
|
||||
|
||||
const onPreviousMonth = async () => {
|
||||
const month = store.month === 0 ? 11 : store.month - 1
|
||||
const year = store.month === 0 ? store.year - 1 : store.year
|
||||
const data = await getCosts(params.id!, year, month)
|
||||
setStore({ month, year, data })
|
||||
setStore({ month, year })
|
||||
}
|
||||
|
||||
const onNextMonth = async () => {
|
||||
const month = store.month === 11 ? 0 : store.month + 1
|
||||
const year = store.month === 11 ? store.year + 1 : store.year
|
||||
setStore({ month, year, data: await getCosts(params.id!, year, month) })
|
||||
setStore({ month, year })
|
||||
}
|
||||
|
||||
const onSelectModel = (model: string | null) => setStore({ model, modelDropdownOpen: false })
|
||||
|
||||
const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false })
|
||||
|
||||
const getData = createMemo(() => store.data ?? initialData())
|
||||
|
||||
const getModels = createMemo(() => {
|
||||
const data = getData()
|
||||
if (!data?.usage) return []
|
||||
return Array.from(new Set(data.usage.map((row) => row.model))).sort()
|
||||
if (!store.data?.usage) return []
|
||||
return Array.from(new Set(store.data.usage.map((row) => row.model))).sort()
|
||||
})
|
||||
|
||||
const getDates = createMemo(() => {
|
||||
@@ -206,10 +199,19 @@ export function GraphSection() {
|
||||
const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth()
|
||||
|
||||
const chartConfig = createMemo((): ChartConfiguration | null => {
|
||||
const data = getData()
|
||||
const data = store.data
|
||||
const dates = getDates()
|
||||
if (!data?.usage?.length) return null
|
||||
|
||||
store.colorScheme
|
||||
const styles = getComputedStyle(document.documentElement)
|
||||
const colorTextMuted = styles.getPropertyValue("--color-text-muted").trim()
|
||||
const colorBorderMuted = styles.getPropertyValue("--color-border-muted").trim()
|
||||
const colorBgElevated = styles.getPropertyValue("--color-bg-elevated").trim()
|
||||
const colorText = styles.getPropertyValue("--color-text").trim()
|
||||
const colorTextSecondary = styles.getPropertyValue("--color-text-secondary").trim()
|
||||
const colorBorder = styles.getPropertyValue("--color-border").trim()
|
||||
|
||||
const dailyData = new Map<string, Map<string, number>>()
|
||||
for (const dateKey of dates) dailyData.set(dateKey, new Map())
|
||||
|
||||
@@ -252,7 +254,7 @@ export function GraphSection() {
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
autoSkipPadding: 20,
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
color: colorTextMuted,
|
||||
font: {
|
||||
family: "monospace",
|
||||
size: 11,
|
||||
@@ -263,10 +265,10 @@ export function GraphSection() {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: "rgba(255, 255, 255, 0.1)",
|
||||
color: colorBorderMuted,
|
||||
},
|
||||
ticks: {
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
color: colorTextMuted,
|
||||
font: {
|
||||
family: "monospace",
|
||||
size: 11,
|
||||
@@ -282,10 +284,10 @@ export function GraphSection() {
|
||||
tooltip: {
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.9)",
|
||||
titleColor: "rgba(255, 255, 255, 0.9)",
|
||||
bodyColor: "rgba(255, 255, 255, 0.8)",
|
||||
borderColor: "rgba(255, 255, 255, 0.1)",
|
||||
backgroundColor: colorBgElevated,
|
||||
titleColor: colorText,
|
||||
bodyColor: colorTextSecondary,
|
||||
borderColor: colorBorder,
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
displayColors: true,
|
||||
@@ -301,7 +303,7 @@ export function GraphSection() {
|
||||
display: true,
|
||||
position: "bottom",
|
||||
labels: {
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
color: colorTextSecondary,
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
@@ -339,15 +341,32 @@ export function GraphSection() {
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(async () => {
|
||||
const data = await getCosts(params.id!, store.year, store.month)
|
||||
setStore({ data })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const config = chartConfig()
|
||||
if (!config || !canvasRef) return
|
||||
|
||||
if (chartInstance) chartInstance.destroy()
|
||||
chartInstance = new Chart(canvasRef, config)
|
||||
|
||||
onCleanup(() => chartInstance?.destroy())
|
||||
})
|
||||
|
||||
onCleanup(() => chartInstance?.destroy())
|
||||
createEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
setStore({ colorScheme: mediaQuery.matches ? "dark" : "light" })
|
||||
|
||||
const handleColorSchemeChange = (e: MediaQueryListEvent) => {
|
||||
setStore({ colorScheme: e.matches ? "dark" : "light" })
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener("change", handleColorSchemeChange)
|
||||
onCleanup(() => mediaQuery.removeEventListener("change", handleColorSchemeChange))
|
||||
})
|
||||
|
||||
return (
|
||||
<section class={styles.root}>
|
||||
@@ -356,55 +375,53 @@ export function GraphSection() {
|
||||
<p>Usage costs broken down by model.</p>
|
||||
</div>
|
||||
|
||||
<Show when={getData()}>
|
||||
<div data-slot="filter-container">
|
||||
<div data-slot="month-picker">
|
||||
<button data-slot="month-button" onClick={onPreviousMonth}>
|
||||
<IconChevronLeft />
|
||||
</button>
|
||||
<span data-slot="month-label">{formatMonthYear()}</span>
|
||||
<button data-slot="month-button" onClick={onNextMonth} disabled={isCurrentMonth()}>
|
||||
<IconChevronRight />
|
||||
</button>
|
||||
</div>
|
||||
<Dropdown
|
||||
trigger={store.model === null ? "All Models" : store.model}
|
||||
open={store.modelDropdownOpen}
|
||||
onOpenChange={(open) => setStore({ modelDropdownOpen: open })}
|
||||
>
|
||||
<>
|
||||
<button data-slot="model-item" onClick={() => onSelectModel(null)}>
|
||||
<span>All Models</span>
|
||||
</button>
|
||||
<For each={getModels()}>
|
||||
{(model) => (
|
||||
<button data-slot="model-item" onClick={() => onSelectModel(model)}>
|
||||
<span>{model}</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
trigger={getKeyName(store.key)}
|
||||
open={store.keyDropdownOpen}
|
||||
onOpenChange={(open) => setStore({ keyDropdownOpen: open })}
|
||||
>
|
||||
<>
|
||||
<button data-slot="model-item" onClick={() => onSelectKey(null)}>
|
||||
<span>All Keys</span>
|
||||
</button>
|
||||
<For each={getData()?.keys || []}>
|
||||
{(key) => (
|
||||
<button data-slot="model-item" onClick={() => onSelectKey(key.id)}>
|
||||
<span>{key.displayName}</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
</Dropdown>
|
||||
<div data-slot="filter-container">
|
||||
<div data-slot="month-picker">
|
||||
<button data-slot="month-button" onClick={onPreviousMonth}>
|
||||
<IconChevronLeft />
|
||||
</button>
|
||||
<span data-slot="month-label">{formatMonthYear()}</span>
|
||||
<button data-slot="month-button" onClick={onNextMonth} disabled={isCurrentMonth()}>
|
||||
<IconChevronRight />
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Dropdown
|
||||
trigger={store.model === null ? "All Models" : store.model}
|
||||
open={store.modelDropdownOpen}
|
||||
onOpenChange={(open) => setStore({ modelDropdownOpen: open })}
|
||||
>
|
||||
<>
|
||||
<button data-slot="model-item" onClick={() => onSelectModel(null)}>
|
||||
<span>All Models</span>
|
||||
</button>
|
||||
<For each={getModels()}>
|
||||
{(model) => (
|
||||
<button data-slot="model-item" onClick={() => onSelectModel(model)}>
|
||||
<span>{model}</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
trigger={getKeyName(store.key)}
|
||||
open={store.keyDropdownOpen}
|
||||
onOpenChange={(open) => setStore({ keyDropdownOpen: open })}
|
||||
>
|
||||
<>
|
||||
<button data-slot="model-item" onClick={() => onSelectKey(null)}>
|
||||
<span>All Keys</span>
|
||||
</button>
|
||||
<For each={store.data?.keys || []}>
|
||||
{(key) => (
|
||||
<button data-slot="model-item" onClick={() => onSelectKey(key.id)}>
|
||||
<span>{key.displayName}</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={chartConfig()}
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
import { Resource, waitUntil } from "@opencode-ai/console-resource"
|
||||
|
||||
export function createDataDumper(sessionId: string, requestId: string) {
|
||||
export function createDataDumper(sessionId: string, requestId: string, projectId: string) {
|
||||
if (Resource.App.stage !== "production") return
|
||||
if (sessionId === "") return
|
||||
|
||||
let data: Record<string, any> = {}
|
||||
let modelName: string | undefined
|
||||
let data: Record<string, any> = { sessionId, requestId, projectId }
|
||||
let metadata: Record<string, any> = { sessionId, requestId, projectId }
|
||||
|
||||
return {
|
||||
provideModel: (model?: string) => (modelName = model),
|
||||
provideModel: (model?: string) => {
|
||||
data.modelName = model
|
||||
metadata.modelName = model
|
||||
},
|
||||
provideRequest: (request: string) => (data.request = request),
|
||||
provideResponse: (response: string) => (data.response = response),
|
||||
provideStream: (chunk: string) => (data.response = (data.response ?? "") + chunk),
|
||||
flush: () => {
|
||||
if (!modelName) return
|
||||
if (!data.modelName) return
|
||||
|
||||
const str = new Date().toISOString().replace(/[^0-9]/g, "")
|
||||
const yyyymmdd = str.substring(0, 8)
|
||||
const hh = str.substring(8, 10)
|
||||
const timestamp = new Date().toISOString().replace(/[^0-9]/g, "")
|
||||
|
||||
waitUntil(
|
||||
Resource.ConsoleData.put(`${yyyymmdd}/${hh}/${modelName}/${sessionId}/${requestId}.json`, JSON.stringify(data)),
|
||||
Resource.ZenData.put(
|
||||
`data/${data.modelName}/${sessionId}/${requestId}.json`,
|
||||
JSON.stringify({ timestamp, ...data }),
|
||||
),
|
||||
)
|
||||
|
||||
waitUntil(
|
||||
Resource.ZenData.put(
|
||||
`meta/${data.modelName}/${timestamp}/${requestId}.json`,
|
||||
JSON.stringify({ timestamp, ...metadata }),
|
||||
),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,13 +13,7 @@ import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
|
||||
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
|
||||
import { logger } from "./logger"
|
||||
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
|
||||
import {
|
||||
createBodyConverter,
|
||||
createStreamPartConverter,
|
||||
createResponseConverter,
|
||||
ProviderHelper,
|
||||
UsageInfo,
|
||||
} from "./provider/provider"
|
||||
import { createBodyConverter, createStreamPartConverter, createResponseConverter, UsageInfo } from "./provider/provider"
|
||||
import { anthropicHelper } from "./provider/anthropic"
|
||||
import { googleHelper } from "./provider/google"
|
||||
import { openaiHelper } from "./provider/openai"
|
||||
@@ -27,6 +21,7 @@ import { oaCompatHelper } from "./provider/openai-compatible"
|
||||
import { createRateLimiter } from "./rateLimiter"
|
||||
import { createDataDumper } from "./dataDumper"
|
||||
import { createTrialLimiter } from "./trialLimiter"
|
||||
import { createStickyTracker } from "./stickyProviderTracker"
|
||||
|
||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||
type RetryOptions = {
|
||||
@@ -61,6 +56,7 @@ export async function handler(
|
||||
const ip = input.request.headers.get("x-real-ip") ?? ""
|
||||
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
|
||||
const requestId = input.request.headers.get("x-opencode-request") ?? ""
|
||||
const projectId = input.request.headers.get("x-opencode-project") ?? ""
|
||||
logger.metric({
|
||||
is_tream: isStream,
|
||||
session: sessionId,
|
||||
@@ -68,14 +64,16 @@ export async function handler(
|
||||
})
|
||||
const zenData = ZenData.list()
|
||||
const modelInfo = validateModel(zenData, model)
|
||||
const dataDumper = createDataDumper(sessionId, requestId)
|
||||
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trial?.limit, ip)
|
||||
const isTrial = await trialLimiter?.isTrial()
|
||||
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
||||
await rateLimiter?.check()
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId)
|
||||
const stickyProvider = await stickyTracker?.get()
|
||||
|
||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||
const providerInfo = selectProvider(zenData, modelInfo, sessionId, isTrial ?? false, retry)
|
||||
const providerInfo = selectProvider(zenData, modelInfo, sessionId, isTrial ?? false, retry, stickyProvider)
|
||||
const authInfo = await authenticate(modelInfo, providerInfo)
|
||||
validateBilling(authInfo, modelInfo)
|
||||
validateModelSettings(authInfo)
|
||||
@@ -126,6 +124,9 @@ export async function handler(
|
||||
dataDumper?.provideModel(providerInfo.storeModel)
|
||||
dataDumper?.provideRequest(reqBody)
|
||||
|
||||
// Store sticky provider
|
||||
await stickyTracker?.set(providerInfo.id)
|
||||
|
||||
// Scrub response headers
|
||||
const resHeaders = new Headers()
|
||||
const keepHeaders = ["content-type", "cache-control"]
|
||||
@@ -294,12 +295,18 @@ export async function handler(
|
||||
sessionId: string,
|
||||
isTrial: boolean,
|
||||
retry: RetryOptions,
|
||||
stickyProvider: string | undefined,
|
||||
) {
|
||||
const provider = (() => {
|
||||
if (isTrial) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
|
||||
}
|
||||
|
||||
if (stickyProvider) {
|
||||
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
|
||||
if (provider) return provider
|
||||
}
|
||||
|
||||
if (retry.retryCount === MAX_RETRIES) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
|
||||
export function createStickyTracker(stickyProvider: boolean, session: string) {
|
||||
if (!stickyProvider) return
|
||||
if (!session) return
|
||||
const key = `sticky:${session}`
|
||||
|
||||
return {
|
||||
get: async () => {
|
||||
return await Resource.GatewayKv.get(key)
|
||||
},
|
||||
set: async (providerId: string) => {
|
||||
await Resource.GatewayKv.put(key, providerId, { expirationTtl: 86400 })
|
||||
},
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -6,4 +6,4 @@
|
||||
/// <reference path="../../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -11,7 +11,7 @@ export namespace Account {
|
||||
id: z.string().optional(),
|
||||
}),
|
||||
async (input) =>
|
||||
Database.transaction(async (tx) => {
|
||||
Database.use(async (tx) => {
|
||||
const id = input.id ?? Identifier.create("account")
|
||||
await tx.insert(AccountTable).values({
|
||||
id,
|
||||
@@ -21,13 +21,12 @@ export namespace Account {
|
||||
)
|
||||
|
||||
export const fromID = fn(z.string(), async (id) =>
|
||||
Database.transaction(async (tx) => {
|
||||
return tx
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(AccountTable)
|
||||
.where(eq(AccountTable.id, id))
|
||||
.execute()
|
||||
.then((rows) => rows[0])
|
||||
}),
|
||||
.then((rows) => rows[0]),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export namespace ZenData {
|
||||
cost: ModelCostSchema,
|
||||
cost200K: ModelCostSchema.optional(),
|
||||
allowAnonymous: z.boolean().optional(),
|
||||
stickyProvider: z.boolean().optional(),
|
||||
trial: z
|
||||
.object({
|
||||
limit: z.number(),
|
||||
|
||||
@@ -47,7 +47,7 @@ export namespace Provider {
|
||||
}),
|
||||
async ({ provider }) => {
|
||||
Actor.assertAdmin()
|
||||
return Database.transaction((tx) =>
|
||||
return Database.use((tx) =>
|
||||
tx
|
||||
.delete(ProviderTable)
|
||||
.where(and(eq(ProviderTable.provider, provider), eq(ProviderTable.workspaceID, Actor.workspace()))),
|
||||
|
||||
Vendored
+94
-90
@@ -6,126 +6,130 @@
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
ADMIN_SECRET: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ADMIN_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
AUTH_API_URL: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
"AUTH_API_URL": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
AWS_SES_ACCESS_KEY_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"AWS_SES_ACCESS_KEY_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
AWS_SES_SECRET_ACCESS_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"AWS_SES_SECRET_ACCESS_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
CLOUDFLARE_API_TOKEN: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"CLOUDFLARE_API_TOKEN": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
Console: {
|
||||
type: "sst.cloudflare.SolidStart"
|
||||
url: string
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
Database: {
|
||||
database: string
|
||||
host: string
|
||||
password: string
|
||||
port: number
|
||||
type: "sst.sst.Linkable"
|
||||
username: string
|
||||
"Database": {
|
||||
"database": string
|
||||
"host": string
|
||||
"password": string
|
||||
"port": number
|
||||
"type": "sst.sst.Linkable"
|
||||
"username": string
|
||||
}
|
||||
Desktop: {
|
||||
type: "sst.cloudflare.StaticSite"
|
||||
url: string
|
||||
"Desktop": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
EMAILOCTOPUS_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"EMAILOCTOPUS_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_APP_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
GITHUB_APP_PRIVATE_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_CLIENT_ID_CONSOLE: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_APP_PRIVATE_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_CLIENT_SECRET_CONSOLE: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_CLIENT_ID_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GOOGLE_CLIENT_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_CLIENT_SECRET_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
HONEYCOMB_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GOOGLE_CLIENT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
R2AccessKey: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"HONEYCOMB_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
R2SecretKey: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"R2AccessKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
STRIPE_SECRET_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"R2SecretKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
STRIPE_WEBHOOK_SECRET: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
"STRIPE_SECRET_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
Web: {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
"STRIPE_WEBHOOK_SECRET": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS1: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
}
|
||||
ZEN_MODELS2: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS3: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS2": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS4: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS3": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS4": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types"
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types";
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
Api: cloudflare.Service
|
||||
AuthApi: cloudflare.Service
|
||||
AuthStorage: cloudflare.KVNamespace
|
||||
Bucket: cloudflare.R2Bucket
|
||||
ConsoleData: cloudflare.R2Bucket
|
||||
EnterpriseStorage: cloudflare.R2Bucket
|
||||
GatewayKv: cloudflare.KVNamespace
|
||||
LogProcessor: cloudflare.Service
|
||||
"Api": cloudflare.Service
|
||||
"AuthApi": cloudflare.Service
|
||||
"AuthStorage": cloudflare.KVNamespace
|
||||
"Bucket": cloudflare.R2Bucket
|
||||
"EnterpriseStorage": cloudflare.R2Bucket
|
||||
"GatewayKv": cloudflare.KVNamespace
|
||||
"LogProcessor": cloudflare.Service
|
||||
"ZenData": cloudflare.R2Bucket
|
||||
}
|
||||
}
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -194,7 +194,7 @@ export default {
|
||||
// Get workspace
|
||||
await Actor.provide("account", { accountID, email }, async () => {
|
||||
await User.joinInvitedWorkspaces()
|
||||
const workspaces = await Database.transaction(async (tx) =>
|
||||
const workspaces = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ id: WorkspaceTable.id })
|
||||
.from(WorkspaceTable)
|
||||
|
||||
+94
-90
@@ -6,126 +6,130 @@
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
ADMIN_SECRET: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ADMIN_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
AUTH_API_URL: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
"AUTH_API_URL": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
AWS_SES_ACCESS_KEY_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"AWS_SES_ACCESS_KEY_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
AWS_SES_SECRET_ACCESS_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"AWS_SES_SECRET_ACCESS_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
CLOUDFLARE_API_TOKEN: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"CLOUDFLARE_API_TOKEN": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
Console: {
|
||||
type: "sst.cloudflare.SolidStart"
|
||||
url: string
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
Database: {
|
||||
database: string
|
||||
host: string
|
||||
password: string
|
||||
port: number
|
||||
type: "sst.sst.Linkable"
|
||||
username: string
|
||||
"Database": {
|
||||
"database": string
|
||||
"host": string
|
||||
"password": string
|
||||
"port": number
|
||||
"type": "sst.sst.Linkable"
|
||||
"username": string
|
||||
}
|
||||
Desktop: {
|
||||
type: "sst.cloudflare.StaticSite"
|
||||
url: string
|
||||
"Desktop": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
EMAILOCTOPUS_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"EMAILOCTOPUS_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_APP_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
GITHUB_APP_PRIVATE_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_CLIENT_ID_CONSOLE: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_APP_PRIVATE_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_CLIENT_SECRET_CONSOLE: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_CLIENT_ID_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GOOGLE_CLIENT_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_CLIENT_SECRET_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
HONEYCOMB_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GOOGLE_CLIENT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
R2AccessKey: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"HONEYCOMB_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
R2SecretKey: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"R2AccessKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
STRIPE_SECRET_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"R2SecretKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
STRIPE_WEBHOOK_SECRET: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
"STRIPE_SECRET_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
Web: {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
"STRIPE_WEBHOOK_SECRET": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS1: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
}
|
||||
ZEN_MODELS2: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS3: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS2": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS4: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS3": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS4": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types"
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types";
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
Api: cloudflare.Service
|
||||
AuthApi: cloudflare.Service
|
||||
AuthStorage: cloudflare.KVNamespace
|
||||
Bucket: cloudflare.R2Bucket
|
||||
ConsoleData: cloudflare.R2Bucket
|
||||
EnterpriseStorage: cloudflare.R2Bucket
|
||||
GatewayKv: cloudflare.KVNamespace
|
||||
LogProcessor: cloudflare.Service
|
||||
"Api": cloudflare.Service
|
||||
"AuthApi": cloudflare.Service
|
||||
"AuthStorage": cloudflare.KVNamespace
|
||||
"Bucket": cloudflare.R2Bucket
|
||||
"EnterpriseStorage": cloudflare.R2Bucket
|
||||
"GatewayKv": cloudflare.KVNamespace
|
||||
"LogProcessor": cloudflare.Service
|
||||
"ZenData": cloudflare.R2Bucket
|
||||
}
|
||||
}
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
Vendored
+1
-1
@@ -6,4 +6,4 @@
|
||||
/// <reference path="../../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
@@ -2,8 +2,8 @@ import type { KVNamespaceListOptions, KVNamespaceListResult, KVNamespacePutOptio
|
||||
import { Resource as ResourceBase } from "sst"
|
||||
import Cloudflare from "cloudflare"
|
||||
|
||||
export const waitUntil = async (fn: () => Promise<void>) => {
|
||||
await fn()
|
||||
export const waitUntil = async (promise: Promise<any>) => {
|
||||
await promise
|
||||
}
|
||||
|
||||
export const Resource = new Proxy(
|
||||
|
||||
+94
-90
@@ -6,126 +6,130 @@
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
ADMIN_SECRET: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ADMIN_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
AUTH_API_URL: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
"AUTH_API_URL": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
AWS_SES_ACCESS_KEY_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"AWS_SES_ACCESS_KEY_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
AWS_SES_SECRET_ACCESS_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"AWS_SES_SECRET_ACCESS_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
CLOUDFLARE_API_TOKEN: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"CLOUDFLARE_API_TOKEN": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
Console: {
|
||||
type: "sst.cloudflare.SolidStart"
|
||||
url: string
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
Database: {
|
||||
database: string
|
||||
host: string
|
||||
password: string
|
||||
port: number
|
||||
type: "sst.sst.Linkable"
|
||||
username: string
|
||||
"Database": {
|
||||
"database": string
|
||||
"host": string
|
||||
"password": string
|
||||
"port": number
|
||||
"type": "sst.sst.Linkable"
|
||||
"username": string
|
||||
}
|
||||
Desktop: {
|
||||
type: "sst.cloudflare.StaticSite"
|
||||
url: string
|
||||
"Desktop": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
EMAILOCTOPUS_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"EMAILOCTOPUS_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_APP_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
GITHUB_APP_PRIVATE_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_CLIENT_ID_CONSOLE: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_APP_PRIVATE_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_CLIENT_SECRET_CONSOLE: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_CLIENT_ID_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GOOGLE_CLIENT_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_CLIENT_SECRET_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
HONEYCOMB_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GOOGLE_CLIENT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
R2AccessKey: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"HONEYCOMB_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
R2SecretKey: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"R2AccessKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
STRIPE_SECRET_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"R2SecretKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
STRIPE_WEBHOOK_SECRET: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
"STRIPE_SECRET_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
Web: {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
"STRIPE_WEBHOOK_SECRET": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS1: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
}
|
||||
ZEN_MODELS2: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS3: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS2": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS4: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS3": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS4": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types"
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types";
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
Api: cloudflare.Service
|
||||
AuthApi: cloudflare.Service
|
||||
AuthStorage: cloudflare.KVNamespace
|
||||
Bucket: cloudflare.R2Bucket
|
||||
ConsoleData: cloudflare.R2Bucket
|
||||
EnterpriseStorage: cloudflare.R2Bucket
|
||||
GatewayKv: cloudflare.KVNamespace
|
||||
LogProcessor: cloudflare.Service
|
||||
"Api": cloudflare.Service
|
||||
"AuthApi": cloudflare.Service
|
||||
"AuthStorage": cloudflare.KVNamespace
|
||||
"Bucket": cloudflare.R2Bucket
|
||||
"EnterpriseStorage": cloudflare.R2Bucket
|
||||
"GatewayKv": cloudflare.KVNamespace
|
||||
"LogProcessor": cloudflare.Service
|
||||
"ZenData": cloudflare.R2Bucket
|
||||
}
|
||||
}
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -456,9 +456,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0" />
|
||||
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
|
||||
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
|
||||
<Show when={i.release_date}>
|
||||
<Show when={false}>
|
||||
<span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
|
||||
{DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
|
||||
{DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useSync } from "@/context/sync"
|
||||
import { useSession } from "@/context/session"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { Diff } from "@opencode-ai/ui/diff"
|
||||
|
||||
export default function Page() {
|
||||
const layout = useLayout()
|
||||
@@ -123,7 +124,6 @@ export default function Page() {
|
||||
const handleTabClick = async (tab: string) => {
|
||||
if (store.clickTimer) {
|
||||
resetClickTimer()
|
||||
// local.file.update(file.path, { ...file, pinned: true })
|
||||
} else {
|
||||
if (tab.startsWith("file://")) {
|
||||
local.file.open(tab.replace("file://", ""))
|
||||
@@ -355,8 +355,15 @@ export default function Page() {
|
||||
classes={{
|
||||
root: "pb-20 flex-1 min-w-0",
|
||||
content: "pb-20",
|
||||
container: "w-full " + (wide() ? "max-w-146 mx-auto px-6" : "pr-6 pl-18"),
|
||||
container:
|
||||
"w-full " +
|
||||
(wide()
|
||||
? "max-w-146 mx-auto px-6"
|
||||
: session.messages.user().length > 1
|
||||
? "pr-6 pl-18"
|
||||
: "px-6"),
|
||||
}}
|
||||
diffComponent={Diff}
|
||||
/>
|
||||
</div>
|
||||
</Match>
|
||||
@@ -405,6 +412,7 @@ export default function Page() {
|
||||
container: "px-6",
|
||||
}}
|
||||
diffs={session.diffs()}
|
||||
diffComponent={Diff}
|
||||
actions={
|
||||
<Tooltip value="Open in tab">
|
||||
<IconButton
|
||||
@@ -436,6 +444,7 @@ export default function Page() {
|
||||
container: "px-6",
|
||||
}}
|
||||
diffs={session.diffs()}
|
||||
diffComponent={Diff}
|
||||
split
|
||||
/>
|
||||
</div>
|
||||
|
||||
Vendored
+4
-2
@@ -2,7 +2,9 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMetaEnv {}
|
||||
interface ImportMetaEnv {
|
||||
|
||||
}
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -6,4 +6,4 @@
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
@@ -18,4 +18,7 @@ export default defineConfig({
|
||||
build: {
|
||||
target: "esnext",
|
||||
},
|
||||
worker: {
|
||||
format: "es",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -18,6 +18,10 @@ import z from "zod"
|
||||
import NotFound from "../[...404]"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
|
||||
import { Diff } from "@opencode-ai/ui/diff-ssr"
|
||||
import { clientOnly } from "@solidjs/start"
|
||||
|
||||
const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff })))
|
||||
|
||||
const SessionDataMissingError = NamedError.create(
|
||||
"SessionDataMissingError",
|
||||
@@ -230,6 +234,7 @@ export default function () {
|
||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||
container: "px-4",
|
||||
}}
|
||||
diffComponent={ClientOnlyDiff}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
@@ -279,7 +284,8 @@ export default function () {
|
||||
classList={{
|
||||
"w-full flex justify-start items-start min-w-0": true,
|
||||
"max-w-146 mx-auto px-6": wide(),
|
||||
"pr-6 pl-18": !wide(),
|
||||
"pr-6 pl-18": !wide() && messages().length > 1,
|
||||
"px-6": !wide() && messages().length === 1,
|
||||
}}
|
||||
>
|
||||
{title()}
|
||||
@@ -297,8 +303,11 @@ export default function () {
|
||||
classes={{
|
||||
root: "grow",
|
||||
content: "flex flex-col justify-between items-start",
|
||||
container: "w-full pb-20 " + (wide() ? "max-w-146 mx-auto px-6" : "pr-6 pl-18"),
|
||||
container:
|
||||
"w-full pb-20 " +
|
||||
(wide() ? "max-w-146 mx-auto px-6" : messages().length > 1 ? "pr-6 pl-18" : "px-6"),
|
||||
}}
|
||||
diffComponent={ClientOnlyDiff}
|
||||
>
|
||||
<div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
@@ -311,6 +320,7 @@ export default function () {
|
||||
<SessionReview
|
||||
class="@4xl:hidden"
|
||||
diffs={diffs()}
|
||||
diffComponent={Diff}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
@@ -318,9 +328,10 @@ export default function () {
|
||||
}}
|
||||
/>
|
||||
<SessionReview
|
||||
class="hidden @4xl:flex"
|
||||
split
|
||||
class="hidden @4xl:flex"
|
||||
diffs={splitDiffs()}
|
||||
diffComponent={Diff}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
@@ -352,6 +363,7 @@ export default function () {
|
||||
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
diffComponent={Diff}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-4",
|
||||
|
||||
Vendored
+94
-90
@@ -6,126 +6,130 @@
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
ADMIN_SECRET: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ADMIN_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
AUTH_API_URL: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
"AUTH_API_URL": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
AWS_SES_ACCESS_KEY_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"AWS_SES_ACCESS_KEY_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
AWS_SES_SECRET_ACCESS_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"AWS_SES_SECRET_ACCESS_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
CLOUDFLARE_API_TOKEN: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"CLOUDFLARE_API_TOKEN": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
Console: {
|
||||
type: "sst.cloudflare.SolidStart"
|
||||
url: string
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
Database: {
|
||||
database: string
|
||||
host: string
|
||||
password: string
|
||||
port: number
|
||||
type: "sst.sst.Linkable"
|
||||
username: string
|
||||
"Database": {
|
||||
"database": string
|
||||
"host": string
|
||||
"password": string
|
||||
"port": number
|
||||
"type": "sst.sst.Linkable"
|
||||
"username": string
|
||||
}
|
||||
Desktop: {
|
||||
type: "sst.cloudflare.StaticSite"
|
||||
url: string
|
||||
"Desktop": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
EMAILOCTOPUS_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"EMAILOCTOPUS_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_APP_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
GITHUB_APP_PRIVATE_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_CLIENT_ID_CONSOLE: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_APP_PRIVATE_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_CLIENT_SECRET_CONSOLE: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_CLIENT_ID_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GOOGLE_CLIENT_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_CLIENT_SECRET_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
HONEYCOMB_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GOOGLE_CLIENT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
R2AccessKey: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"HONEYCOMB_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
R2SecretKey: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"R2AccessKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
STRIPE_SECRET_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"R2SecretKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
STRIPE_WEBHOOK_SECRET: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
"STRIPE_SECRET_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
Web: {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
"STRIPE_WEBHOOK_SECRET": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS1: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
}
|
||||
ZEN_MODELS2: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS3: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS2": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS4: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS3": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS4": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types"
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types";
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
Api: cloudflare.Service
|
||||
AuthApi: cloudflare.Service
|
||||
AuthStorage: cloudflare.KVNamespace
|
||||
Bucket: cloudflare.R2Bucket
|
||||
ConsoleData: cloudflare.R2Bucket
|
||||
EnterpriseStorage: cloudflare.R2Bucket
|
||||
GatewayKv: cloudflare.KVNamespace
|
||||
LogProcessor: cloudflare.Service
|
||||
"Api": cloudflare.Service
|
||||
"AuthApi": cloudflare.Service
|
||||
"AuthStorage": cloudflare.KVNamespace
|
||||
"Bucket": cloudflare.R2Bucket
|
||||
"EnterpriseStorage": cloudflare.R2Bucket
|
||||
"GatewayKv": cloudflare.KVNamespace
|
||||
"LogProcessor": cloudflare.Service
|
||||
"ZenData": cloudflare.R2Bucket
|
||||
}
|
||||
}
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
@@ -23,4 +23,7 @@ export default defineConfig({
|
||||
host: "0.0.0.0",
|
||||
allowedHosts: true,
|
||||
},
|
||||
worker: {
|
||||
format: "es",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The AI coding agent built for the terminal"
|
||||
version = "1.0.126"
|
||||
version = "1.0.133"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/sst/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.126/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.133/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.126/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.133/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.126/opencode-linux-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.133/opencode-linux-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.126/opencode-linux-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.133/opencode-linux-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.126/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.133/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
Vendored
+94
-90
@@ -6,126 +6,130 @@
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
ADMIN_SECRET: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ADMIN_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
AUTH_API_URL: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
"AUTH_API_URL": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
AWS_SES_ACCESS_KEY_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"AWS_SES_ACCESS_KEY_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
AWS_SES_SECRET_ACCESS_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"AWS_SES_SECRET_ACCESS_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
CLOUDFLARE_API_TOKEN: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"CLOUDFLARE_API_TOKEN": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
Console: {
|
||||
type: "sst.cloudflare.SolidStart"
|
||||
url: string
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
Database: {
|
||||
database: string
|
||||
host: string
|
||||
password: string
|
||||
port: number
|
||||
type: "sst.sst.Linkable"
|
||||
username: string
|
||||
"Database": {
|
||||
"database": string
|
||||
"host": string
|
||||
"password": string
|
||||
"port": number
|
||||
"type": "sst.sst.Linkable"
|
||||
"username": string
|
||||
}
|
||||
Desktop: {
|
||||
type: "sst.cloudflare.StaticSite"
|
||||
url: string
|
||||
"Desktop": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
EMAILOCTOPUS_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"EMAILOCTOPUS_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_APP_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
GITHUB_APP_PRIVATE_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_CLIENT_ID_CONSOLE: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_APP_PRIVATE_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GITHUB_CLIENT_SECRET_CONSOLE: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_CLIENT_ID_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
GOOGLE_CLIENT_ID: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GITHUB_CLIENT_SECRET_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
HONEYCOMB_API_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"GOOGLE_CLIENT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
R2AccessKey: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"HONEYCOMB_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
R2SecretKey: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"R2AccessKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
STRIPE_SECRET_KEY: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"R2SecretKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
STRIPE_WEBHOOK_SECRET: {
|
||||
type: "sst.sst.Linkable"
|
||||
value: string
|
||||
"STRIPE_SECRET_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
Web: {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
"STRIPE_WEBHOOK_SECRET": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS1: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
}
|
||||
ZEN_MODELS2: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS1": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS3: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS2": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
ZEN_MODELS4: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
"ZEN_MODELS3": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS4": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types"
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types";
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
Api: cloudflare.Service
|
||||
AuthApi: cloudflare.Service
|
||||
AuthStorage: cloudflare.KVNamespace
|
||||
Bucket: cloudflare.R2Bucket
|
||||
ConsoleData: cloudflare.R2Bucket
|
||||
EnterpriseStorage: cloudflare.R2Bucket
|
||||
GatewayKv: cloudflare.KVNamespace
|
||||
LogProcessor: cloudflare.Service
|
||||
"Api": cloudflare.Service
|
||||
"AuthApi": cloudflare.Service
|
||||
"AuthStorage": cloudflare.KVNamespace
|
||||
"Bucket": cloudflare.R2Bucket
|
||||
"EnterpriseStorage": cloudflare.R2Bucket
|
||||
"GatewayKv": cloudflare.KVNamespace
|
||||
"LogProcessor": cloudflare.Service
|
||||
"ZenData": cloudflare.R2Bucket
|
||||
}
|
||||
}
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.126",
|
||||
"version": "1.0.133",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -64,8 +64,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.2.8",
|
||||
"@opentui/core": "0.1.54",
|
||||
"@opentui/solid": "0.1.54",
|
||||
"@opentui/core": "0.1.56",
|
||||
"@opentui/solid": "0.1.56",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
|
||||
@@ -16,6 +16,7 @@ import pkg from "../package.json"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
|
||||
const singleFlag = process.argv.includes("--single")
|
||||
const skipInstall = process.argv.includes("--skip-install")
|
||||
|
||||
const allTargets: {
|
||||
os: string
|
||||
@@ -83,8 +84,10 @@ const targets = singleFlag
|
||||
await $`rm -rf dist`
|
||||
|
||||
const binaries: Record<string, string> = {}
|
||||
await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
||||
await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
|
||||
if (!skipInstall) {
|
||||
await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
||||
await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
|
||||
}
|
||||
for (const item of targets) {
|
||||
const name = [
|
||||
pkg.name,
|
||||
@@ -102,6 +105,10 @@ for (const item of targets) {
|
||||
const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"))
|
||||
const workerPath = "./src/cli/cmd/tui/worker.ts"
|
||||
|
||||
// Use platform-specific bunfs root path based on target OS
|
||||
const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
|
||||
const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/")
|
||||
|
||||
await Bun.build({
|
||||
conditions: ["browser"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
@@ -118,7 +125,7 @@ for (const item of targets) {
|
||||
entrypoints: ["./src/index.ts", parserWorker, workerPath],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${Script.version}'`,
|
||||
OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(dir, parserWorker).replaceAll("\\", "/"),
|
||||
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
||||
OPENCODE_WORKER_PATH: workerPath,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
},
|
||||
|
||||
@@ -224,6 +224,7 @@ export namespace Agent {
|
||||
export async function generate(input: { description: string }) {
|
||||
const defaultModel = await Provider.defaultModel()
|
||||
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
|
||||
const language = await Provider.getLanguage(model)
|
||||
const system = SystemPrompt.header(defaultModel.providerID)
|
||||
system.push(PROMPT_GENERATE)
|
||||
const existing = await list()
|
||||
@@ -241,7 +242,7 @@ export namespace Agent {
|
||||
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
|
||||
},
|
||||
],
|
||||
model: model.language,
|
||||
model: language,
|
||||
schema: z.object({
|
||||
identifier: z.string(),
|
||||
whenToUse: z.string(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ModelsDev } from "../../provider/models"
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Config } from "../../config/config"
|
||||
import { Global } from "../../global"
|
||||
import { Plugin } from "../../plugin"
|
||||
import { Instance } from "../../project/instance"
|
||||
@@ -103,7 +104,22 @@ export const AuthLoginCommand = cmd({
|
||||
return
|
||||
}
|
||||
await ModelsDev.refresh().catch(() => {})
|
||||
const providers = await ModelsDev.get()
|
||||
|
||||
const config = await Config.get()
|
||||
|
||||
const disabled = new Set(config.disabled_providers ?? [])
|
||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||
|
||||
const providers = await ModelsDev.get().then((x) => {
|
||||
const filtered: Record<string, (typeof x)[string]> = {}
|
||||
for (const [key, value] of Object.entries(x)) {
|
||||
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
|
||||
filtered[key] = value
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
})
|
||||
|
||||
const priority: Record<string, number> = {
|
||||
opencode: 0,
|
||||
anthropic: 1,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CommandModule } from "yargs"
|
||||
|
||||
export function cmd<T, U>(input: CommandModule<T, U>) {
|
||||
type WithDoubleDash<T> = T & { "--"?: string[] }
|
||||
|
||||
export function cmd<T, U>(input: CommandModule<T, WithDoubleDash<U>>) {
|
||||
return input
|
||||
}
|
||||
|
||||
@@ -562,6 +562,11 @@ export const GithubRunCommand = cmd({
|
||||
}
|
||||
|
||||
async function getUserPrompt() {
|
||||
const customPrompt = process.env["PROMPT"]
|
||||
if (customPrompt) {
|
||||
return { userPrompt: customPrompt, promptFiles: [] }
|
||||
}
|
||||
|
||||
const reviewContext = getReviewCommentContext()
|
||||
let prompt = (() => {
|
||||
const body = payload.comment.body.trim()
|
||||
|
||||
@@ -38,7 +38,7 @@ export const ModelsCommand = cmd({
|
||||
|
||||
function printModels(providerID: string, verbose?: boolean) {
|
||||
const provider = providers[providerID]
|
||||
const sortedModels = Object.entries(provider.info.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
for (const [modelID, model] of sortedModels) {
|
||||
process.stdout.write(`${providerID}/${modelID}`)
|
||||
process.stdout.write(EOL)
|
||||
|
||||
@@ -88,7 +88,7 @@ export const RunCommand = cmd({
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
let message = args.message.join(" ")
|
||||
let message = [...args.message, ...(args["--"] || [])].join(" ")
|
||||
|
||||
const fileParts: any[] = []
|
||||
if (args.file) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { cmd } from "./cmd"
|
||||
import { Session } from "../../session"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { UI } from "../ui"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { EOL } from "os"
|
||||
|
||||
export const SessionCommand = cmd({
|
||||
command: "session",
|
||||
describe: "manage sessions",
|
||||
builder: (yargs: Argv) => yargs.command(SessionListCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
export const SessionListCommand = cmd({
|
||||
command: "list",
|
||||
describe: "list sessions",
|
||||
builder: (yargs: Argv) => {
|
||||
return yargs
|
||||
.option("max-count", {
|
||||
alias: "n",
|
||||
describe: "limit to N most recent sessions",
|
||||
type: "number",
|
||||
})
|
||||
.option("format", {
|
||||
describe: "output format",
|
||||
type: "string",
|
||||
choices: ["table", "json"],
|
||||
default: "table",
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const sessions = []
|
||||
for await (const session of Session.list()) {
|
||||
if (!session.parentID) {
|
||||
sessions.push(session)
|
||||
}
|
||||
}
|
||||
|
||||
sessions.sort((a, b) => b.time.updated - a.time.updated)
|
||||
|
||||
const limitedSessions = args.maxCount ? sessions.slice(0, args.maxCount) : sessions
|
||||
|
||||
if (limitedSessions.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let output: string
|
||||
if (args.format === "json") {
|
||||
output = formatSessionJSON(limitedSessions)
|
||||
} else {
|
||||
output = formatSessionTable(limitedSessions)
|
||||
}
|
||||
|
||||
const shouldPaginate = process.stdout.isTTY && !args.maxCount && args.format === "table"
|
||||
|
||||
if (shouldPaginate) {
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["less", "-R", "-S"],
|
||||
stdin: "pipe",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
|
||||
proc.stdin.write(output)
|
||||
proc.stdin.end()
|
||||
await proc.exited
|
||||
} else {
|
||||
console.log(output)
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
function formatSessionTable(sessions: Session.Info[]): string {
|
||||
const lines: string[] = []
|
||||
|
||||
const maxIdWidth = Math.max(20, ...sessions.map((s) => s.id.length))
|
||||
const maxTitleWidth = Math.max(25, ...sessions.map((s) => s.title.length))
|
||||
|
||||
const header = `Session ID${" ".repeat(maxIdWidth - 10)} Title${" ".repeat(maxTitleWidth - 5)} Updated`
|
||||
lines.push(header)
|
||||
lines.push("─".repeat(header.length))
|
||||
for (const session of sessions) {
|
||||
const truncatedTitle = Locale.truncate(session.title, maxTitleWidth)
|
||||
const timeStr = Locale.todayTimeOrDateTime(session.time.updated)
|
||||
const line = `${session.id.padEnd(maxIdWidth)} ${truncatedTitle.padEnd(maxTitleWidth)} ${timeStr}`
|
||||
lines.push(line)
|
||||
}
|
||||
|
||||
return lines.join(EOL)
|
||||
}
|
||||
|
||||
function formatSessionJSON(sessions: Session.Info[]): string {
|
||||
const jsonData = sessions.map((session) => ({
|
||||
id: session.id,
|
||||
title: session.title,
|
||||
updated: session.time.updated,
|
||||
created: session.time.created,
|
||||
projectId: session.projectID,
|
||||
directory: session.directory,
|
||||
}))
|
||||
return JSON.stringify(jsonData, null, 2)
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show } from "solid-js"
|
||||
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
|
||||
import { Installation } from "@/installation"
|
||||
import { Global } from "@/global"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
||||
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
|
||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||
@@ -31,6 +32,7 @@ import { KVProvider, useKV } from "./context/kv"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ArgsProvider, useArgs, type Args } from "./context/args"
|
||||
import open from "open"
|
||||
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
||||
|
||||
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
// can't set raw mode if not a TTY
|
||||
@@ -118,7 +120,9 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<PromptHistoryProvider>
|
||||
<App />
|
||||
<PromptRefProvider>
|
||||
<App />
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
@@ -159,6 +163,7 @@ function App() {
|
||||
const { theme, mode, setMode } = useTheme()
|
||||
const sync = useSync()
|
||||
const exit = useExit()
|
||||
const promptRef = usePromptRef()
|
||||
|
||||
createEffect(() => {
|
||||
console.log(JSON.stringify(route.data))
|
||||
@@ -197,6 +202,17 @@ function App() {
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => sync.status === "complete" && sync.data.provider.length === 0,
|
||||
(isEmpty, wasEmpty) => {
|
||||
// only trigger when we transition into an empty-provider state
|
||||
if (!isEmpty || wasEmpty) return
|
||||
dialog.replace(() => <DialogProviderList />)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
command.register(() => [
|
||||
{
|
||||
title: "Switch session",
|
||||
@@ -213,8 +229,12 @@ function App() {
|
||||
keybind: "session_new",
|
||||
category: "Session",
|
||||
onSelect: () => {
|
||||
const current = promptRef.current
|
||||
// Don't require focus - if there's any text, preserve it
|
||||
const currentPrompt = current?.current?.input ? current.current : undefined
|
||||
route.navigate({
|
||||
type: "home",
|
||||
initialPrompt: currentPrompt,
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -301,10 +321,11 @@ function App() {
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`,
|
||||
title: "Toggle appearance",
|
||||
value: "theme.switch_mode",
|
||||
onSelect: () => {
|
||||
onSelect: (dialog) => {
|
||||
setMode(mode() === "dark" ? "light" : "dark")
|
||||
dialog.clear()
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
@@ -367,8 +388,9 @@ function App() {
|
||||
])
|
||||
|
||||
createEffect(() => {
|
||||
const providerID = local.model.current().providerID
|
||||
if (providerID === "openrouter" && !kv.get("openrouter_warning", false)) {
|
||||
const currentModel = local.model.current()
|
||||
if (!currentModel) return
|
||||
if (currentModel.providerID === "openrouter" && !kv.get("openrouter_warning", false)) {
|
||||
untrack(() => {
|
||||
DialogAlert.show(
|
||||
dialog,
|
||||
@@ -448,6 +470,10 @@ function App() {
|
||||
height={dimensions().height}
|
||||
backgroundColor={theme.background}
|
||||
onMouseUp={async () => {
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) {
|
||||
renderer.clearSelection()
|
||||
return
|
||||
}
|
||||
const text = renderer.getSelection()?.getSelectedText()
|
||||
if (text && text.length > 0) {
|
||||
const base64 = Buffer.from(text).toString("base64")
|
||||
|
||||
@@ -352,8 +352,8 @@ export function Autocomplete(props: {
|
||||
function select() {
|
||||
const selected = options()[store.selected]
|
||||
if (!selected) return
|
||||
selected.onSelect?.()
|
||||
hide()
|
||||
selected.onSelect?.()
|
||||
}
|
||||
|
||||
function show(mode: "@" | "/") {
|
||||
@@ -374,6 +374,10 @@ export function Autocomplete(props: {
|
||||
if (store.visible === "/" && !text.endsWith(" ") && text.startsWith("/")) {
|
||||
const cursor = props.input().logicalCursor
|
||||
props.input().deleteRange(0, 0, cursor.row, cursor.col)
|
||||
// Sync the prompt store immediately since onContentChange is async
|
||||
props.setPrompt((draft) => {
|
||||
draft.input = props.input().plainText
|
||||
})
|
||||
}
|
||||
command.keybinds(true)
|
||||
setStore("visible", false)
|
||||
|
||||
@@ -22,6 +22,9 @@ import { TuiEvent } from "../../event"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { createColors, createFrames } from "../../ui/spinner.ts"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
|
||||
import { useToast } from "../../ui/toast"
|
||||
|
||||
export type PromptProps = {
|
||||
sessionID?: string
|
||||
@@ -34,6 +37,7 @@ export type PromptProps = {
|
||||
|
||||
export type PromptRef = {
|
||||
focused: boolean
|
||||
current: PromptInfo
|
||||
set(prompt: PromptInfo): void
|
||||
reset(): void
|
||||
blur(): void
|
||||
@@ -50,12 +54,25 @@ export function Prompt(props: PromptProps) {
|
||||
const sdk = useSDK()
|
||||
const route = useRoute()
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const toast = useToast()
|
||||
const status = createMemo(() => sync.data.session_status[props.sessionID ?? ""] ?? { type: "idle" })
|
||||
const history = usePromptHistory()
|
||||
const command = useCommandDialog()
|
||||
const renderer = useRenderer()
|
||||
const { theme, syntax } = useTheme()
|
||||
|
||||
function promptModelWarning() {
|
||||
toast.show({
|
||||
variant: "warning",
|
||||
message: "Connect a provider to send prompts",
|
||||
duration: 3000,
|
||||
})
|
||||
if (sync.data.provider.length === 0) {
|
||||
dialog.replace(() => <DialogProviderConnect />)
|
||||
}
|
||||
}
|
||||
|
||||
const textareaKeybindings = createMemo(() => {
|
||||
const newlineBindings = keybind.all.input_newline || []
|
||||
const submitBindings = keybind.all.input_submit || []
|
||||
@@ -253,7 +270,7 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
createEffect(() => {
|
||||
if (props.disabled) input.cursorColor = theme.backgroundElement
|
||||
if (!props.disabled) input.cursorColor = theme.primary
|
||||
if (!props.disabled) input.cursorColor = theme.text
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
@@ -361,6 +378,9 @@ export function Prompt(props: PromptProps) {
|
||||
get focused() {
|
||||
return input.focused
|
||||
},
|
||||
get current() {
|
||||
return store.prompt
|
||||
},
|
||||
focus() {
|
||||
input.focus()
|
||||
},
|
||||
@@ -388,6 +408,11 @@ export function Prompt(props: PromptProps) {
|
||||
if (props.disabled) return
|
||||
if (autocomplete.visible) return
|
||||
if (!store.prompt.input) return
|
||||
const selectedModel = local.model.current()
|
||||
if (!selectedModel) {
|
||||
promptModelWarning()
|
||||
return
|
||||
}
|
||||
const sessionID = props.sessionID
|
||||
? props.sessionID
|
||||
: await (async () => {
|
||||
@@ -424,8 +449,8 @@ export function Prompt(props: PromptProps) {
|
||||
body: {
|
||||
agent: local.agent.current().name,
|
||||
model: {
|
||||
providerID: local.model.current().providerID,
|
||||
modelID: local.model.current().modelID,
|
||||
providerID: selectedModel.providerID,
|
||||
modelID: selectedModel.modelID,
|
||||
},
|
||||
command: inputText,
|
||||
},
|
||||
@@ -448,7 +473,7 @@ export function Prompt(props: PromptProps) {
|
||||
command: command.slice(1),
|
||||
arguments: args.join(" "),
|
||||
agent: local.agent.current().name,
|
||||
model: `${local.model.current().providerID}/${local.model.current().modelID}`,
|
||||
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
|
||||
messageID,
|
||||
},
|
||||
})
|
||||
@@ -458,10 +483,10 @@ export function Prompt(props: PromptProps) {
|
||||
id: sessionID,
|
||||
},
|
||||
body: {
|
||||
...local.model.current(),
|
||||
...selectedModel,
|
||||
messageID,
|
||||
agent: local.agent.current().name,
|
||||
model: local.model.current(),
|
||||
model: selectedModel,
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
@@ -784,12 +809,12 @@ export function Prompt(props: PromptProps) {
|
||||
ref={(r: TextareaRenderable) => {
|
||||
input = r
|
||||
setTimeout(() => {
|
||||
input.cursorColor = highlight()
|
||||
input.cursorColor = theme.text
|
||||
}, 0)
|
||||
}}
|
||||
onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
||||
focusedBackgroundColor={theme.backgroundElement}
|
||||
cursorColor={highlight()}
|
||||
cursorColor={theme.text}
|
||||
syntaxStyle={syntax()}
|
||||
/>
|
||||
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
|
||||
|
||||
@@ -175,8 +175,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
const provider = sync.data.provider[0]
|
||||
const model = sync.data.provider_default[provider.id] ?? Object.values(provider.models)[0].id
|
||||
if (!provider) return undefined
|
||||
const defaultModel = sync.data.provider_default[provider.id]
|
||||
const firstModel = Object.values(provider.models)[0]
|
||||
const model = defaultModel ?? firstModel?.id
|
||||
if (!model) return undefined
|
||||
return {
|
||||
providerID: provider.id,
|
||||
modelID: model,
|
||||
@@ -185,11 +190,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const currentModel = createMemo(() => {
|
||||
const a = agent.current()
|
||||
return getFirstValidModel(
|
||||
() => modelStore.model[a.name],
|
||||
() => a.model,
|
||||
fallbackModel,
|
||||
)!
|
||||
return (
|
||||
getFirstValidModel(
|
||||
() => modelStore.model[a.name],
|
||||
() => a.model,
|
||||
fallbackModel,
|
||||
) ?? undefined
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -205,11 +212,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
},
|
||||
parsed: createMemo(() => {
|
||||
const value = currentModel()
|
||||
const provider = sync.data.provider.find((x) => x.id === value.providerID)!
|
||||
const model = provider.models[value.modelID]
|
||||
if (!value) {
|
||||
return {
|
||||
provider: "Connect a provider",
|
||||
model: "No provider selected",
|
||||
}
|
||||
}
|
||||
const provider = sync.data.provider.find((x) => x.id === value.providerID)
|
||||
const info = provider?.models[value.modelID]
|
||||
return {
|
||||
provider: provider.name ?? value.providerID,
|
||||
model: model.name ?? value.modelID,
|
||||
provider: provider?.name ?? value.providerID,
|
||||
model: info?.name ?? value.modelID,
|
||||
}
|
||||
}),
|
||||
cycle(direction: 1 | -1) {
|
||||
@@ -236,7 +249,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
return
|
||||
}
|
||||
const current = currentModel()
|
||||
let index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
||||
let index = -1
|
||||
if (current) {
|
||||
index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
||||
}
|
||||
if (index === -1) {
|
||||
index = direction === 1 ? 0 : favorites.length - 1
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createSimpleContext } from "./helper"
|
||||
import type { PromptRef } from "../component/prompt"
|
||||
|
||||
export const { use: usePromptRef, provider: PromptRefProvider } = createSimpleContext({
|
||||
name: "PromptRef",
|
||||
init: () => {
|
||||
let current: PromptRef | undefined
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return current
|
||||
},
|
||||
set(ref: PromptRef | undefined) {
|
||||
current = ref
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import type { PromptInfo } from "../component/prompt/history"
|
||||
|
||||
export type HomeRoute = {
|
||||
type: "home"
|
||||
initialPrompt?: PromptInfo
|
||||
}
|
||||
|
||||
export type SessionRoute = {
|
||||
|
||||
@@ -161,7 +161,7 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||
|
||||
if (c.startsWith("#")) return RGBA.fromHex(c)
|
||||
|
||||
if (defs[c]) {
|
||||
if (defs[c] != null) {
|
||||
return resolveColor(defs[c])
|
||||
} else if (theme.theme[c as keyof ThemeColors] !== undefined) {
|
||||
return resolveColor(theme.theme[c as keyof ThemeColors]!)
|
||||
@@ -169,6 +169,9 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||
throw new Error(`Color reference "${c}" not found in defs or theme`)
|
||||
}
|
||||
}
|
||||
if (typeof c === "number") {
|
||||
return ansiToRgba(c)
|
||||
}
|
||||
return resolveColor(c[mode])
|
||||
}
|
||||
|
||||
@@ -203,6 +206,51 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||
} as Theme
|
||||
}
|
||||
|
||||
function ansiToRgba(code: number): RGBA {
|
||||
// Standard ANSI colors (0-15)
|
||||
if (code < 16) {
|
||||
const ansiColors = [
|
||||
"#000000", // Black
|
||||
"#800000", // Red
|
||||
"#008000", // Green
|
||||
"#808000", // Yellow
|
||||
"#000080", // Blue
|
||||
"#800080", // Magenta
|
||||
"#008080", // Cyan
|
||||
"#c0c0c0", // White
|
||||
"#808080", // Bright Black
|
||||
"#ff0000", // Bright Red
|
||||
"#00ff00", // Bright Green
|
||||
"#ffff00", // Bright Yellow
|
||||
"#0000ff", // Bright Blue
|
||||
"#ff00ff", // Bright Magenta
|
||||
"#00ffff", // Bright Cyan
|
||||
"#ffffff", // Bright White
|
||||
]
|
||||
return RGBA.fromHex(ansiColors[code] ?? "#000000")
|
||||
}
|
||||
|
||||
// 6x6x6 Color Cube (16-231)
|
||||
if (code < 232) {
|
||||
const index = code - 16
|
||||
const b = index % 6
|
||||
const g = Math.floor(index / 6) % 6
|
||||
const r = Math.floor(index / 36)
|
||||
|
||||
const val = (x: number) => (x === 0 ? 0 : x * 40 + 55)
|
||||
return RGBA.fromInts(val(r), val(g), val(b))
|
||||
}
|
||||
|
||||
// Grayscale Ramp (232-255)
|
||||
if (code < 256) {
|
||||
const gray = (code - 232) * 10 + 8
|
||||
return RGBA.fromInts(gray, gray, gray)
|
||||
}
|
||||
|
||||
// Fallback for invalid codes
|
||||
return RGBA.fromInts(0, 0, 0)
|
||||
}
|
||||
|
||||
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
name: "Theme",
|
||||
init: (props: { mode: "dark" | "light" }) => {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||
import { createMemo, Match, onMount, Show, Switch, type ParentProps } from "solid-js"
|
||||
import { createMemo, Match, onMount, Show, Switch } from "solid-js"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
import type { KeybindsConfig } from "@opencode-ai/sdk"
|
||||
import { Logo } from "../component/logo"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { useSync } from "../context/sync"
|
||||
import { Toast } from "../ui/toast"
|
||||
import { useArgs } from "../context/args"
|
||||
import { Global } from "@/global"
|
||||
import { useDirectory } from "../context/directory"
|
||||
import { useRoute, useRouteData } from "@tui/context/route"
|
||||
import { usePromptRef } from "../context/prompt"
|
||||
|
||||
// TODO: what is the best way to do this?
|
||||
let once = false
|
||||
@@ -17,6 +16,8 @@ let once = false
|
||||
export function Home() {
|
||||
const sync = useSync()
|
||||
const { theme } = useTheme()
|
||||
const route = useRouteData("home")
|
||||
const promptRef = usePromptRef()
|
||||
const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0)
|
||||
const mcpError = createMemo(() => {
|
||||
return Object.values(sync.data.mcp).some((x) => x.status === "failed")
|
||||
@@ -45,7 +46,10 @@ export function Home() {
|
||||
const args = useArgs()
|
||||
onMount(() => {
|
||||
if (once) return
|
||||
if (args.prompt) {
|
||||
if (route.initialPrompt) {
|
||||
prompt.set(route.initialPrompt)
|
||||
once = true
|
||||
} else if (args.prompt) {
|
||||
prompt.set({ input: args.prompt, parts: [] })
|
||||
once = true
|
||||
}
|
||||
@@ -57,7 +61,13 @@ export function Home() {
|
||||
<box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<Logo />
|
||||
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}>
|
||||
<Prompt ref={(r) => (prompt = r)} hint={Hint} />
|
||||
<Prompt
|
||||
ref={(r) => {
|
||||
prompt = r
|
||||
promptRef.set(r)
|
||||
}}
|
||||
hint={Hint}
|
||||
/>
|
||||
</box>
|
||||
<Toast />
|
||||
</box>
|
||||
|
||||
@@ -63,6 +63,7 @@ import { useKV } from "../../context/kv.tsx"
|
||||
import { Editor } from "../../util/editor"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import { Footer } from "./footer.tsx"
|
||||
import { usePromptRef } from "../../context/prompt"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -81,6 +82,8 @@ const context = createContext<{
|
||||
conceal: () => boolean
|
||||
showThinking: () => boolean
|
||||
showTimestamps: () => boolean
|
||||
usernameVisible: () => boolean
|
||||
showDetails: () => boolean
|
||||
diffWrapMode: () => "word" | "none"
|
||||
sync: ReturnType<typeof useSync>
|
||||
}>()
|
||||
@@ -97,6 +100,7 @@ export function Session() {
|
||||
const sync = useSync()
|
||||
const kv = useKV()
|
||||
const { theme } = useTheme()
|
||||
const promptRef = usePromptRef()
|
||||
const session = createMemo(() => sync.session.get(route.sessionID)!)
|
||||
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
|
||||
const permissions = createMemo(() => sync.data.permission[route.sessionID] ?? [])
|
||||
@@ -114,6 +118,8 @@ export function Session() {
|
||||
const [conceal, setConceal] = createSignal(true)
|
||||
const [showThinking, setShowThinking] = createSignal(kv.get("thinking_visibility", true))
|
||||
const [showTimestamps, setShowTimestamps] = createSignal(kv.get("timestamps", "hide") === "show")
|
||||
const [usernameVisible, setUsernameVisible] = createSignal(kv.get("username_visible", true))
|
||||
const [showDetails, setShowDetails] = createSignal(kv.get("tool_details_visibility", true))
|
||||
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
|
||||
|
||||
const wide = createMemo(() => dimensions().width > 120)
|
||||
@@ -269,13 +275,22 @@ export function Session() {
|
||||
keybind: "session_compact",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
const selectedModel = local.model.current()
|
||||
if (!selectedModel) {
|
||||
toast.show({
|
||||
variant: "warning",
|
||||
message: "Connect a provider to summarize this session",
|
||||
duration: 3000,
|
||||
})
|
||||
return
|
||||
}
|
||||
sdk.client.session.summarize({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
body: {
|
||||
modelID: local.model.current().modelID,
|
||||
providerID: local.model.current().providerID,
|
||||
modelID: selectedModel.modelID,
|
||||
providerID: selectedModel.providerID,
|
||||
},
|
||||
})
|
||||
dialog.clear()
|
||||
@@ -408,6 +423,20 @@ export function Session() {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: usernameVisible() ? "Hide username" : "Show username",
|
||||
value: "session.username_visible.toggle",
|
||||
keybind: "username_toggle",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
setUsernameVisible((prev) => {
|
||||
const next = !prev
|
||||
kv.set("username_visible", next)
|
||||
return next
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Toggle code concealment",
|
||||
value: "session.toggle.conceal",
|
||||
@@ -453,6 +482,18 @@ export function Session() {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: showDetails() ? "Hide tool details" : "Show tool details",
|
||||
value: "session.toggle.actions",
|
||||
keybind: "tool_details",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
const newValue = !showDetails()
|
||||
setShowDetails(newValue)
|
||||
kv.set("tool_details_visibility", newValue)
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Page up",
|
||||
value: "session.page.up",
|
||||
@@ -519,6 +560,37 @@ export function Session() {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Jump to last user message",
|
||||
value: "session.messages_last_user",
|
||||
keybind: "messages_last_user",
|
||||
category: "Session",
|
||||
onSelect: () => {
|
||||
const messages = sync.data.message[route.sessionID]
|
||||
if (!messages || !messages.length) return
|
||||
|
||||
// Find the most recent user message with non-ignored, non-synthetic text parts
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i]
|
||||
if (!message || message.role !== "user") continue
|
||||
|
||||
const parts = sync.data.part[message.id]
|
||||
if (!parts || !Array.isArray(parts)) continue
|
||||
|
||||
const hasValidTextPart = parts.some(
|
||||
(part) => part && part.type === "text" && !part.synthetic && !part.ignored,
|
||||
)
|
||||
|
||||
if (hasValidTextPart) {
|
||||
const child = scroll.getChildren().find((child) => {
|
||||
return child.id === message.id
|
||||
})
|
||||
if (child) scroll.scrollBy(child.y - scroll.y - 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Copy last assistant message",
|
||||
value: "messages.copy",
|
||||
@@ -754,6 +826,8 @@ export function Session() {
|
||||
conceal,
|
||||
showThinking,
|
||||
showTimestamps,
|
||||
usernameVisible,
|
||||
showDetails,
|
||||
diffWrapMode,
|
||||
sync,
|
||||
}}
|
||||
@@ -877,7 +951,10 @@ export function Session() {
|
||||
</scrollbox>
|
||||
<box flexShrink={0}>
|
||||
<Prompt
|
||||
ref={(r) => (prompt = r)}
|
||||
ref={(r) => {
|
||||
prompt = r
|
||||
promptRef.set(r)
|
||||
}}
|
||||
disabled={permissions().length > 0}
|
||||
onSubmit={() => {
|
||||
toBottom()
|
||||
@@ -917,13 +994,14 @@ function UserMessage(props: {
|
||||
pending?: string
|
||||
}) {
|
||||
const ctx = use()
|
||||
const local = useLocal()
|
||||
const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
|
||||
const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
|
||||
const sync = useSync()
|
||||
const { theme } = useTheme()
|
||||
const [hover, setHover] = createSignal(false)
|
||||
const queued = createMemo(() => props.pending && props.message.id > props.pending)
|
||||
const color = createMemo(() => (queued() ? theme.accent : theme.secondary))
|
||||
const color = createMemo(() => (queued() ? theme.accent : local.agent.color(props.message.agent)))
|
||||
|
||||
const compaction = createMemo(() => props.parts.find((x) => x.type === "compaction"))
|
||||
|
||||
@@ -972,7 +1050,7 @@ function UserMessage(props: {
|
||||
</box>
|
||||
</Show>
|
||||
<text fg={theme.textMuted}>
|
||||
{sync.data.config.username ?? "You"}{" "}
|
||||
{ctx.usernameVisible() ? `${sync.data.config.username ?? "You"} ` : "You"}{" "}
|
||||
<Show
|
||||
when={queued()}
|
||||
fallback={
|
||||
@@ -1078,7 +1156,11 @@ const PART_MAPPING = {
|
||||
function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) {
|
||||
const { theme, subtleSyntax } = useTheme()
|
||||
const ctx = use()
|
||||
const content = createMemo(() => props.part.text.trim())
|
||||
const content = createMemo(() => {
|
||||
// Filter out redacted reasoning chunks from OpenRouter
|
||||
// OpenRouter sends encrypted reasoning data that appears as [REDACTED]
|
||||
return props.part.text.replace("[REDACTED]", "").trim()
|
||||
})
|
||||
return (
|
||||
<Show when={content() && ctx.showThinking()}>
|
||||
<box
|
||||
@@ -1128,9 +1210,21 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
|
||||
|
||||
function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMessage }) {
|
||||
const { theme } = useTheme()
|
||||
const { showDetails } = use()
|
||||
const sync = useSync()
|
||||
const [margin, setMargin] = createSignal(0)
|
||||
const component = createMemo(() => {
|
||||
// Hide tool if showDetails is false and tool completed successfully
|
||||
// But always show if there's an error or permission is required
|
||||
const shouldHide =
|
||||
!showDetails() &&
|
||||
props.part.state.status === "completed" &&
|
||||
!sync.data.permission[props.message.sessionID]?.some((x) => x.callID === props.part.callID)
|
||||
|
||||
if (shouldHide) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const render = ToolRegistry.render(props.part.tool) ?? GenericTool
|
||||
|
||||
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Installation } from "@/installation"
|
||||
import { Server } from "@/server/server"
|
||||
import { Log } from "@/util/log"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { Rpc } from "@/util/rpc"
|
||||
import { upgrade } from "@/cli/upgrade"
|
||||
|
||||
@@ -43,6 +44,7 @@ export const rpc = {
|
||||
async checkUpgrade(input: { directory: string }) {
|
||||
await Instance.provide({
|
||||
directory: input.directory,
|
||||
init: InstanceBootstrap,
|
||||
fn: async () => {
|
||||
await upgrade().catch(() => {})
|
||||
},
|
||||
|
||||
@@ -5,7 +5,8 @@ import { Installation } from "@/installation"
|
||||
|
||||
export async function upgrade() {
|
||||
const config = await Config.global()
|
||||
const latest = await Installation.latest().catch(() => {})
|
||||
const method = await Installation.method()
|
||||
const latest = await Installation.latest(method).catch(() => {})
|
||||
if (!latest) return
|
||||
if (Installation.VERSION === latest) return
|
||||
|
||||
@@ -17,7 +18,6 @@ export async function upgrade() {
|
||||
return
|
||||
}
|
||||
|
||||
const method = await Installation.method()
|
||||
if (method === "unknown") return
|
||||
await Installation.upgrade(method, latest)
|
||||
.then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import z from "zod"
|
||||
import { Config } from "../config/config"
|
||||
import { Instance } from "../project/instance"
|
||||
import PROMPT_INITIALIZE from "./template/initialize.txt"
|
||||
import { Bus } from "../bus"
|
||||
import { Identifier } from "../id/id"
|
||||
import PROMPT_INITIALIZE from "./template/initialize.txt"
|
||||
import PROMPT_REVIEW from "./template/review.txt"
|
||||
|
||||
export namespace Command {
|
||||
export const Default = {
|
||||
INIT: "init",
|
||||
} as const
|
||||
|
||||
export const Event = {
|
||||
Executed: Bus.event(
|
||||
"command.executed",
|
||||
@@ -36,10 +33,27 @@ export namespace Command {
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const Default = {
|
||||
INIT: "init",
|
||||
REVIEW: "review",
|
||||
} as const
|
||||
|
||||
const state = Instance.state(async () => {
|
||||
const cfg = await Config.get()
|
||||
|
||||
const result: Record<string, Info> = {}
|
||||
const result: Record<string, Info> = {
|
||||
[Default.INIT]: {
|
||||
name: Default.INIT,
|
||||
description: "create/update AGENTS.md",
|
||||
template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
|
||||
},
|
||||
[Default.REVIEW]: {
|
||||
name: Default.REVIEW,
|
||||
description: "review changes [commit|branch|pr], defaults to uncommitted",
|
||||
template: PROMPT_REVIEW.replace("${path}", Instance.worktree),
|
||||
subtask: true,
|
||||
},
|
||||
}
|
||||
|
||||
for (const [name, command] of Object.entries(cfg.command ?? {})) {
|
||||
result[name] = {
|
||||
@@ -52,14 +66,6 @@ export namespace Command {
|
||||
}
|
||||
}
|
||||
|
||||
if (result[Default.INIT] === undefined) {
|
||||
result[Default.INIT] = {
|
||||
name: Default.INIT,
|
||||
description: "create/update AGENTS.md",
|
||||
template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
You are a code reviewer. Your job is to review code changes and provide actionable feedback.
|
||||
|
||||
---
|
||||
|
||||
Input: $ARGUMENTS
|
||||
|
||||
---
|
||||
|
||||
## Determining What to Review
|
||||
|
||||
Based on the input provided, determine which type of review to perform:
|
||||
|
||||
1. **No arguments (default)**: Review all uncommitted changes
|
||||
- Run: `git diff` for unstaged changes
|
||||
- Run: `git diff --cached` for staged changes
|
||||
|
||||
2. **Commit hash** (40-char SHA or short hash): Review that specific commit
|
||||
- Run: `git show $ARGUMENTS`
|
||||
|
||||
3. **Branch name**: Compare current branch to the specified branch
|
||||
- Run: `git diff $ARGUMENTS...HEAD`
|
||||
|
||||
4. **PR URL or number** (contains "github.com" or "pull" or looks like a PR number): Review the pull request
|
||||
- Run: `gh pr view $ARGUMENTS` to get PR context
|
||||
- Run: `gh pr diff $ARGUMENTS` to get the diff
|
||||
|
||||
Use best judgement when processing input.
|
||||
|
||||
---
|
||||
|
||||
## What to Look For
|
||||
|
||||
**Bugs** - Your primary focus.
|
||||
- Logic errors, off-by-one mistakes, incorrect conditionals
|
||||
- Edge cases: null/empty inputs, error conditions, race conditions
|
||||
- Security issues: injection, auth bypass, data exposure
|
||||
- Broken error handling that swallows failures
|
||||
|
||||
**Structure** - Does the code fit the codebase?
|
||||
- Does it follow existing patterns and conventions?
|
||||
- Are there established abstractions it should use but doesn't?
|
||||
|
||||
**Performance** - Only flag if obviously problematic.
|
||||
- O(n²) on unbounded data, N+1 queries, blocking I/O on hot paths
|
||||
|
||||
## Before You Flag Something
|
||||
|
||||
Be certain. If you're going to call something a bug, you need to be confident it actually is one.
|
||||
|
||||
- Only review the changes - do not review pre-existing code that wasn't modified
|
||||
- Don't flag something as a bug if you're unsure - investigate first
|
||||
- Don't flag style preferences as issues
|
||||
- Don't invent hypothetical problems - if an edge case matters, explain the realistic scenario where it breaks
|
||||
- If you need more context to be sure, use the tools below to get it
|
||||
|
||||
## Tools
|
||||
|
||||
Use these to inform your review:
|
||||
|
||||
- **Explore agent** - Find how existing code handles similar problems. Check patterns, conventions, and prior art before claiming something doesn't fit.
|
||||
- **Exa Code Context** - Verify correct usage of libraries/APIs before flagging something as wrong.
|
||||
- **Exa Web Search** - Research best practices if you're unsure about a pattern.
|
||||
|
||||
If you're uncertain about something and can't verify it with these tools, say "I'm not sure about X" rather than flagging it as a definite issue.
|
||||
|
||||
## Tone and Approach
|
||||
|
||||
1. If there is a bug, be direct and clear about why it is a bug.
|
||||
2. You should clearly communicate severity of issues, do not claim issues are more severe than they actually are.
|
||||
3. Critiques should clearly and explicitly communicate the scenarios, environments, or inputs that are necessary for the bug to arise. The comment should immediately indicate that the issue's severity depends on these factors.
|
||||
4. Your tone should be matter-of-fact and not accusatory or overly positive. It should read as a helpful AI assistant suggestion without sounding too much like a human reviewer.
|
||||
5. Write in a manner that allows reader to quickly understand issue without reading too closely.
|
||||
6. AVOID flattery, do not give any comments that are not helpful to the reader. Avoid phrasing like "Great job ...", "Thanks for ...".
|
||||
@@ -398,6 +398,7 @@ export namespace Config {
|
||||
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
||||
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
||||
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
|
||||
username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
|
||||
status_view: z.string().optional().default("<leader>s").describe("View status"),
|
||||
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
|
||||
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
|
||||
@@ -417,6 +418,7 @@ export namespace Config {
|
||||
.describe("Scroll messages down by half page"),
|
||||
messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
|
||||
messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
|
||||
messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
|
||||
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
|
||||
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
|
||||
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
|
||||
@@ -425,6 +427,7 @@ export namespace Config {
|
||||
.optional()
|
||||
.default("<leader>h")
|
||||
.describe("Toggle code block concealment in messages"),
|
||||
tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"),
|
||||
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
||||
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
|
||||
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
|
||||
@@ -467,6 +470,42 @@ export namespace Config {
|
||||
})
|
||||
export type Layout = z.infer<typeof Layout>
|
||||
|
||||
export const Provider = ModelsDev.Provider.partial()
|
||||
.extend({
|
||||
whitelist: z.array(z.string()).optional(),
|
||||
blacklist: z.array(z.string()).optional(),
|
||||
models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
|
||||
options: z
|
||||
.object({
|
||||
apiKey: z.string().optional(),
|
||||
baseURL: z.string().optional(),
|
||||
enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
|
||||
setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"),
|
||||
timeout: z
|
||||
.union([
|
||||
z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe(
|
||||
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
||||
),
|
||||
z.literal(false).describe("Disable timeout for this provider entirely."),
|
||||
])
|
||||
.optional()
|
||||
.describe(
|
||||
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
||||
),
|
||||
})
|
||||
.catchall(z.any())
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
ref: "ProviderConfig",
|
||||
})
|
||||
export type Provider = z.infer<typeof Provider>
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
@@ -533,43 +572,7 @@ export namespace Config {
|
||||
.optional()
|
||||
.describe("Agent configuration, see https://opencode.ai/docs/agent"),
|
||||
provider: z
|
||||
.record(
|
||||
z.string(),
|
||||
ModelsDev.Provider.partial()
|
||||
.extend({
|
||||
whitelist: z.array(z.string()).optional(),
|
||||
blacklist: z.array(z.string()).optional(),
|
||||
models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
|
||||
options: z
|
||||
.object({
|
||||
apiKey: z.string().optional(),
|
||||
baseURL: z.string().optional(),
|
||||
enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
|
||||
setCacheKey: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable promptCacheKey for this provider (default false)"),
|
||||
timeout: z
|
||||
.union([
|
||||
z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe(
|
||||
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
||||
),
|
||||
z.literal(false).describe("Disable timeout for this provider entirely."),
|
||||
])
|
||||
.optional()
|
||||
.describe(
|
||||
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
||||
),
|
||||
})
|
||||
.catchall(z.any())
|
||||
.optional(),
|
||||
})
|
||||
.strict(),
|
||||
)
|
||||
.record(z.string(), Provider)
|
||||
.optional()
|
||||
.describe("Custom provider configurations and model overrides"),
|
||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||
@@ -667,6 +670,10 @@ export namespace Config {
|
||||
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
|
||||
disable_paste_summary: z.boolean().optional(),
|
||||
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
|
||||
primary_tools: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Tools that should only be available to primary agents."),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export namespace Env {
|
||||
const state = Instance.state(() => {
|
||||
return { ...process.env } as Record<string, string | undefined>
|
||||
})
|
||||
|
||||
export function get(key: string) {
|
||||
const env = state()
|
||||
return env[key]
|
||||
}
|
||||
|
||||
export function all() {
|
||||
return state()
|
||||
}
|
||||
|
||||
export function set(key: string, value: string) {
|
||||
const env = state()
|
||||
env[key] = value
|
||||
}
|
||||
|
||||
export function remove(key: string) {
|
||||
const env = state()
|
||||
delete env[key]
|
||||
}
|
||||
}
|
||||
@@ -276,11 +276,16 @@ export namespace File {
|
||||
const project = Instance.project
|
||||
let ignored = (_: string) => false
|
||||
if (project.vcs === "git") {
|
||||
const ig = ignore()
|
||||
const gitignore = Bun.file(path.join(Instance.worktree, ".gitignore"))
|
||||
if (await gitignore.exists()) {
|
||||
const ig = ignore().add(await gitignore.text())
|
||||
ignored = ig.ignores.bind(ig)
|
||||
ig.add(await gitignore.text())
|
||||
}
|
||||
const ignoreFile = Bun.file(path.join(Instance.worktree, ".ignore"))
|
||||
if (await ignoreFile.exists()) {
|
||||
ig.add(await ignoreFile.text())
|
||||
}
|
||||
ignored = ig.ignores.bind(ig)
|
||||
}
|
||||
const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory
|
||||
const nodes: Node[] = []
|
||||
|
||||
@@ -17,6 +17,7 @@ export namespace Flag {
|
||||
// Experimental
|
||||
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
|
||||
export const OPENCODE_EXPERIMENTAL_WATCHER = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WATCHER")
|
||||
export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
|
||||
|
||||
function truthy(key: string) {
|
||||
const value = process.env[key]?.toLowerCase()
|
||||
|
||||
@@ -30,7 +30,7 @@ await Promise.all([
|
||||
fs.mkdir(Global.Path.bin, { recursive: true }),
|
||||
])
|
||||
|
||||
const CACHE_VERSION = "12"
|
||||
const CACHE_VERSION = "13"
|
||||
|
||||
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
||||
.text()
|
||||
|
||||
@@ -25,6 +25,7 @@ import { AcpCommand } from "./cli/cmd/acp"
|
||||
import { EOL } from "os"
|
||||
import { WebCommand } from "./cli/cmd/web"
|
||||
import { PrCommand } from "./cli/cmd/pr"
|
||||
import { SessionCommand } from "./cli/cmd/session"
|
||||
|
||||
process.on("unhandledRejection", (e) => {
|
||||
Log.Default.error("rejection", {
|
||||
@@ -39,6 +40,7 @@ process.on("uncaughtException", (e) => {
|
||||
})
|
||||
|
||||
const cli = yargs(hideBin(process.argv))
|
||||
.parserConfiguration({ "populate--": true })
|
||||
.scriptName("opencode")
|
||||
.help("help", "show help")
|
||||
.alias("help", "h")
|
||||
@@ -92,6 +94,7 @@ const cli = yargs(hideBin(process.argv))
|
||||
.command(ImportCommand)
|
||||
.command(GithubCommand)
|
||||
.command(PrCommand)
|
||||
.command(SessionCommand)
|
||||
.fail((msg) => {
|
||||
if (
|
||||
msg.startsWith("Unknown argument") ||
|
||||
|
||||
@@ -4,6 +4,7 @@ import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { Bus } from "../bus"
|
||||
import { Log } from "../util/log"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_VERSION: string
|
||||
@@ -162,10 +163,28 @@ export namespace Installation {
|
||||
export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
|
||||
export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}`
|
||||
|
||||
export async function latest() {
|
||||
export async function latest(installMethod?: Method) {
|
||||
const detectedMethod = installMethod || (await method())
|
||||
if (detectedMethod === "brew") {
|
||||
const formula = await getBrewFormula()
|
||||
if (formula === "opencode") {
|
||||
return fetch("https://formulae.brew.sh/api/formula/opencode.json")
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(res.statusText)
|
||||
return res.json()
|
||||
})
|
||||
.then((data: any) => data.versions.stable)
|
||||
}
|
||||
}
|
||||
|
||||
const registry = await iife(async () => {
|
||||
const r = (await $`npm config get registry`.quiet().nothrow().text()).trim()
|
||||
const reg = r || "https://registry.npmjs.org"
|
||||
return reg.endsWith("/") ? reg.slice(0, -1) : reg
|
||||
})
|
||||
const [major] = VERSION.split(".").map((x) => Number(x))
|
||||
const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
|
||||
return fetch(`https://registry.npmjs.org/opencode-ai/${channel}`)
|
||||
return fetch(`${registry}/opencode-ai/${channel}`)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(res.statusText)
|
||||
return res.json()
|
||||
|
||||
@@ -104,7 +104,7 @@ export namespace LSPClient {
|
||||
},
|
||||
},
|
||||
}),
|
||||
5_000,
|
||||
45_000,
|
||||
).catch((err) => {
|
||||
l.error("initialize error", { error: err })
|
||||
throw new InitializeError(
|
||||
|
||||
@@ -28,8 +28,8 @@ export namespace Plugin {
|
||||
}
|
||||
const plugins = [...(config.plugin ?? [])]
|
||||
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
||||
plugins.push("opencode-copilot-auth@0.0.7")
|
||||
plugins.push("opencode-anthropic-auth@0.0.3")
|
||||
plugins.push("opencode-copilot-auth@0.0.8")
|
||||
plugins.push("opencode-anthropic-auth@0.0.4")
|
||||
}
|
||||
for (let plugin of plugins) {
|
||||
log.info("loading plugin", { path: plugin })
|
||||
|
||||
@@ -9,16 +9,16 @@ export namespace ModelsDev {
|
||||
const log = Log.create({ service: "models.dev" })
|
||||
const filepath = path.join(Global.Path.cache, "models.json")
|
||||
|
||||
export const Model = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
release_date: z.string(),
|
||||
attachment: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
temperature: z.boolean(),
|
||||
tool_call: z.boolean(),
|
||||
cost: z.object({
|
||||
export const Model = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
release_date: z.string(),
|
||||
attachment: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
temperature: z.boolean(),
|
||||
tool_call: z.boolean(),
|
||||
cost: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache_read: z.number().optional(),
|
||||
@@ -31,40 +31,34 @@ export namespace ModelsDev {
|
||||
cache_write: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
limit: z.object({
|
||||
context: z.number(),
|
||||
output: z.number(),
|
||||
}),
|
||||
modalities: z
|
||||
.object({
|
||||
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z.boolean().optional(),
|
||||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||
options: z.record(z.string(), z.any()),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
provider: z.object({ npm: z.string() }).optional(),
|
||||
})
|
||||
.meta({
|
||||
ref: "Model",
|
||||
})
|
||||
})
|
||||
.optional(),
|
||||
limit: z.object({
|
||||
context: z.number(),
|
||||
output: z.number(),
|
||||
}),
|
||||
modalities: z
|
||||
.object({
|
||||
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z.boolean().optional(),
|
||||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||
options: z.record(z.string(), z.any()),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
provider: z.object({ npm: z.string() }).optional(),
|
||||
})
|
||||
export type Model = z.infer<typeof Model>
|
||||
|
||||
export const Provider = z
|
||||
.object({
|
||||
api: z.string().optional(),
|
||||
name: z.string(),
|
||||
env: z.array(z.string()),
|
||||
id: z.string(),
|
||||
npm: z.string().optional(),
|
||||
models: z.record(z.string(), Model),
|
||||
})
|
||||
.meta({
|
||||
ref: "Provider",
|
||||
})
|
||||
export const Provider = z.object({
|
||||
api: z.string().optional(),
|
||||
name: z.string(),
|
||||
env: z.array(z.string()),
|
||||
id: z.string(),
|
||||
npm: z.string().optional(),
|
||||
models: z.record(z.string(), Model),
|
||||
})
|
||||
|
||||
export type Provider = z.infer<typeof Provider>
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import z from "zod"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { Config } from "../config/config"
|
||||
import { mergeDeep, sortBy } from "remeda"
|
||||
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
||||
import { mapValues, mergeDeep, sortBy } from "remeda"
|
||||
import { NoSuchModelError, type Provider as SDK } from "ai"
|
||||
import { Log } from "../util/log"
|
||||
import { BunProc } from "../bun"
|
||||
import { Plugin } from "../plugin"
|
||||
import { ModelsDev } from "./models"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { Auth } from "../auth"
|
||||
import { Env } from "../env"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { iife } from "@/util/iife"
|
||||
@@ -22,7 +23,7 @@ import { createVertex } from "@ai-sdk/google-vertex"
|
||||
import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||
import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider"
|
||||
import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
|
||||
|
||||
export namespace Provider {
|
||||
@@ -42,14 +43,13 @@ export namespace Provider {
|
||||
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
|
||||
}
|
||||
|
||||
type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
|
||||
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
||||
type CustomLoader = (provider: Info) => Promise<{
|
||||
autoload: boolean
|
||||
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
||||
getModel?: CustomModelLoader
|
||||
options?: Record<string, any>
|
||||
}>
|
||||
|
||||
type Source = "env" | "config" | "custom" | "api"
|
||||
|
||||
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
||||
async anthropic() {
|
||||
return {
|
||||
@@ -64,7 +64,8 @@ export namespace Provider {
|
||||
},
|
||||
async opencode(input) {
|
||||
const hasKey = await (async () => {
|
||||
if (input.env.some((item) => process.env[item])) return true
|
||||
const env = Env.all()
|
||||
if (input.env.some((item) => env[item])) return true
|
||||
if (await Auth.get(input.id)) return true
|
||||
return false
|
||||
})()
|
||||
@@ -128,7 +129,7 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
"azure-cognitive-services": async () => {
|
||||
const resourceName = process.env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"]
|
||||
const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
||||
@@ -144,10 +145,15 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
|
||||
return { autoload: false }
|
||||
const [awsProfile, awsAccessKeyId, awsBearerToken, awsRegion] = await Promise.all([
|
||||
Env.get("AWS_PROFILE"),
|
||||
Env.get("AWS_ACCESS_KEY_ID"),
|
||||
Env.get("AWS_BEARER_TOKEN_BEDROCK"),
|
||||
Env.get("AWS_REGION"),
|
||||
])
|
||||
if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false }
|
||||
|
||||
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
||||
const region = awsRegion ?? "us-east-1"
|
||||
|
||||
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
|
||||
return {
|
||||
@@ -246,8 +252,8 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
"google-vertex": async () => {
|
||||
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
|
||||
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5"
|
||||
const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
|
||||
const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-east5"
|
||||
const autoload = Boolean(project)
|
||||
if (!autoload) return { autoload: false }
|
||||
return {
|
||||
@@ -263,8 +269,8 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
"google-vertex-anthropic": async () => {
|
||||
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
|
||||
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "global"
|
||||
const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
|
||||
const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global"
|
||||
const autoload = Boolean(project)
|
||||
if (!autoload) return { autoload: false }
|
||||
return {
|
||||
@@ -273,12 +279,23 @@ export namespace Provider {
|
||||
project,
|
||||
location,
|
||||
},
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
async getModel(sdk, modelID) {
|
||||
const id = String(modelID).trim()
|
||||
return sdk.languageModel(id)
|
||||
},
|
||||
}
|
||||
},
|
||||
"sap-ai-core": async () => {
|
||||
const auth = await Auth.get("sap-ai-core")
|
||||
const serviceKey = Env.get("SAP_AI_SERVICE_KEY") || (auth?.type === "api" ? auth.key : undefined)
|
||||
const deploymentId = Env.get("SAP_AI_DEPLOYMENT_ID") || "d65d81e7c077e583"
|
||||
const resourceGroup = Env.get("SAP_AI_RESOURCE_GROUP") || "default"
|
||||
|
||||
return {
|
||||
autoload: !!serviceKey,
|
||||
options: serviceKey ? { serviceKey, deploymentId, resourceGroup } : {},
|
||||
}
|
||||
},
|
||||
zenmux: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
@@ -292,10 +309,156 @@ export namespace Provider {
|
||||
},
|
||||
}
|
||||
|
||||
export const Model = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
providerID: z.string(),
|
||||
api: z.object({
|
||||
id: z.string(),
|
||||
url: z.string(),
|
||||
npm: z.string(),
|
||||
}),
|
||||
name: z.string(),
|
||||
capabilities: z.object({
|
||||
temperature: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
attachment: z.boolean(),
|
||||
toolcall: z.boolean(),
|
||||
input: z.object({
|
||||
text: z.boolean(),
|
||||
audio: z.boolean(),
|
||||
image: z.boolean(),
|
||||
video: z.boolean(),
|
||||
pdf: z.boolean(),
|
||||
}),
|
||||
output: z.object({
|
||||
text: z.boolean(),
|
||||
audio: z.boolean(),
|
||||
image: z.boolean(),
|
||||
video: z.boolean(),
|
||||
pdf: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
cost: z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache: z.object({
|
||||
read: z.number(),
|
||||
write: z.number(),
|
||||
}),
|
||||
experimentalOver200K: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache: z.object({
|
||||
read: z.number(),
|
||||
write: z.number(),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
limit: z.object({
|
||||
context: z.number(),
|
||||
output: z.number(),
|
||||
}),
|
||||
status: z.enum(["alpha", "beta", "deprecated", "active"]),
|
||||
options: z.record(z.string(), z.any()),
|
||||
headers: z.record(z.string(), z.string()),
|
||||
})
|
||||
.meta({
|
||||
ref: "Model",
|
||||
})
|
||||
export type Model = z.infer<typeof Model>
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
source: z.enum(["env", "config", "custom", "api"]),
|
||||
env: z.string().array(),
|
||||
key: z.string().optional(),
|
||||
options: z.record(z.string(), z.any()),
|
||||
models: z.record(z.string(), Model),
|
||||
})
|
||||
.meta({
|
||||
ref: "Provider",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
||||
return {
|
||||
id: model.id,
|
||||
providerID: provider.id,
|
||||
name: model.name,
|
||||
api: {
|
||||
id: model.id,
|
||||
url: provider.api!,
|
||||
npm: model.provider?.npm ?? provider.npm ?? provider.id,
|
||||
},
|
||||
status: model.status ?? "active",
|
||||
headers: model.headers ?? {},
|
||||
options: model.options ?? {},
|
||||
cost: {
|
||||
input: model.cost?.input ?? 0,
|
||||
output: model.cost?.output ?? 0,
|
||||
cache: {
|
||||
read: model.cost?.cache_read ?? 0,
|
||||
write: model.cost?.cache_write ?? 0,
|
||||
},
|
||||
experimentalOver200K: model.cost?.context_over_200k
|
||||
? {
|
||||
cache: {
|
||||
read: model.cost.context_over_200k.cache_read ?? 0,
|
||||
write: model.cost.context_over_200k.cache_write ?? 0,
|
||||
},
|
||||
input: model.cost.context_over_200k.input,
|
||||
output: model.cost.context_over_200k.output,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
limit: {
|
||||
context: model.limit.context,
|
||||
output: model.limit.output,
|
||||
},
|
||||
capabilities: {
|
||||
temperature: model.temperature,
|
||||
reasoning: model.reasoning,
|
||||
attachment: model.attachment,
|
||||
toolcall: model.tool_call,
|
||||
input: {
|
||||
text: model.modalities?.input?.includes("text") ?? false,
|
||||
audio: model.modalities?.input?.includes("audio") ?? false,
|
||||
image: model.modalities?.input?.includes("image") ?? false,
|
||||
video: model.modalities?.input?.includes("video") ?? false,
|
||||
pdf: model.modalities?.input?.includes("pdf") ?? false,
|
||||
},
|
||||
output: {
|
||||
text: model.modalities?.output?.includes("text") ?? false,
|
||||
audio: model.modalities?.output?.includes("audio") ?? false,
|
||||
image: model.modalities?.output?.includes("image") ?? false,
|
||||
video: model.modalities?.output?.includes("video") ?? false,
|
||||
pdf: model.modalities?.output?.includes("pdf") ?? false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||
return {
|
||||
id: provider.id,
|
||||
source: "custom",
|
||||
name: provider.name,
|
||||
env: provider.env ?? [],
|
||||
options: {},
|
||||
models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)),
|
||||
}
|
||||
}
|
||||
|
||||
const state = Instance.state(async () => {
|
||||
using _ = log.time("state")
|
||||
const config = await Config.get()
|
||||
const database = await ModelsDev.get()
|
||||
const modelsDev = await ModelsDev.get()
|
||||
const database = mapValues(modelsDev, fromModelsDevProvider)
|
||||
|
||||
const disabled = new Set(config.disabled_providers ?? [])
|
||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
|
||||
@@ -306,54 +469,15 @@ export namespace Provider {
|
||||
return true
|
||||
}
|
||||
|
||||
const providers: {
|
||||
[providerID: string]: {
|
||||
source: Source
|
||||
info: ModelsDev.Provider
|
||||
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
||||
options: Record<string, any>
|
||||
}
|
||||
const providers: { [providerID: string]: Info } = {}
|
||||
const languages = new Map<string, LanguageModelV2>()
|
||||
const modelLoaders: {
|
||||
[providerID: string]: CustomModelLoader
|
||||
} = {}
|
||||
const models = new Map<
|
||||
string,
|
||||
{
|
||||
providerID: string
|
||||
modelID: string
|
||||
info: ModelsDev.Model
|
||||
language: LanguageModel
|
||||
npm?: string
|
||||
}
|
||||
>()
|
||||
const sdk = new Map<number, SDK>()
|
||||
// Maps `${provider}/${key}` to the provider’s actual model ID for custom aliases.
|
||||
const realIdByKey = new Map<string, string>()
|
||||
|
||||
log.info("init")
|
||||
|
||||
function mergeProvider(
|
||||
id: string,
|
||||
options: Record<string, any>,
|
||||
source: Source,
|
||||
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>,
|
||||
) {
|
||||
const provider = providers[id]
|
||||
if (!provider) {
|
||||
const info = database[id]
|
||||
if (!info) return
|
||||
if (info.api && !options["baseURL"]) options["baseURL"] = info.api
|
||||
providers[id] = {
|
||||
source,
|
||||
info,
|
||||
options,
|
||||
getModel,
|
||||
}
|
||||
return
|
||||
}
|
||||
provider.options = mergeDeep(provider.options, options)
|
||||
provider.source = source
|
||||
provider.getModel = getModel ?? provider.getModel
|
||||
}
|
||||
|
||||
const configProviders = Object.entries(config.provider ?? {})
|
||||
|
||||
// Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot
|
||||
@@ -363,95 +487,112 @@ export namespace Provider {
|
||||
...githubCopilot,
|
||||
id: "github-copilot-enterprise",
|
||||
name: "GitHub Copilot Enterprise",
|
||||
// Enterprise uses a different API endpoint - will be set dynamically based on auth
|
||||
api: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function mergeProvider(providerID: string, provider: Partial<Info>) {
|
||||
const existing = providers[providerID]
|
||||
if (existing) {
|
||||
// @ts-expect-error
|
||||
providers[providerID] = mergeDeep(existing, provider)
|
||||
return
|
||||
}
|
||||
const match = database[providerID]
|
||||
if (!match) return
|
||||
// @ts-expect-error
|
||||
providers[providerID] = mergeDeep(match, provider)
|
||||
}
|
||||
|
||||
// extend database from config
|
||||
for (const [providerID, provider] of configProviders) {
|
||||
const existing = database[providerID]
|
||||
const parsed: ModelsDev.Provider = {
|
||||
const parsed: Info = {
|
||||
id: providerID,
|
||||
npm: provider.npm ?? existing?.npm,
|
||||
name: provider.name ?? existing?.name ?? providerID,
|
||||
env: provider.env ?? existing?.env ?? [],
|
||||
api: provider.api ?? existing?.api,
|
||||
options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
|
||||
source: "config",
|
||||
models: existing?.models ?? {},
|
||||
}
|
||||
|
||||
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
|
||||
const existing = parsed.models[model.id ?? modelID]
|
||||
const existingModel = parsed.models[model.id ?? modelID]
|
||||
const name = iife(() => {
|
||||
if (model.name) return model.name
|
||||
if (model.id && model.id !== modelID) return modelID
|
||||
return existing?.name ?? modelID
|
||||
return existingModel?.name ?? modelID
|
||||
})
|
||||
const parsedModel: ModelsDev.Model = {
|
||||
const parsedModel: Model = {
|
||||
id: modelID,
|
||||
name,
|
||||
release_date: model.release_date ?? existing?.release_date,
|
||||
attachment: model.attachment ?? existing?.attachment ?? false,
|
||||
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
||||
temperature: model.temperature ?? existing?.temperature ?? false,
|
||||
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
||||
cost:
|
||||
!model.cost && !existing?.cost
|
||||
? {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
}
|
||||
: {
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
...existing?.cost,
|
||||
...model.cost,
|
||||
},
|
||||
options: {
|
||||
...existing?.options,
|
||||
...model.options,
|
||||
api: {
|
||||
id: model.id ?? existingModel?.api.id ?? modelID,
|
||||
npm:
|
||||
model.provider?.npm ?? provider.npm ?? existingModel?.api.npm ?? modelsDev[providerID]?.npm ?? providerID,
|
||||
url: provider?.api ?? existingModel?.api.url,
|
||||
},
|
||||
limit: model.limit ??
|
||||
existing?.limit ?? {
|
||||
context: 0,
|
||||
output: 0,
|
||||
status: model.status ?? existingModel?.status ?? "active",
|
||||
name,
|
||||
providerID,
|
||||
capabilities: {
|
||||
temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
|
||||
reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
|
||||
attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
|
||||
toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
|
||||
input: {
|
||||
text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true,
|
||||
audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false,
|
||||
image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false,
|
||||
video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false,
|
||||
pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
|
||||
},
|
||||
modalities: model.modalities ??
|
||||
existing?.modalities ?? {
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
output: {
|
||||
text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
|
||||
audio: model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false,
|
||||
image: model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false,
|
||||
video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
|
||||
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
|
||||
},
|
||||
headers: model.headers,
|
||||
provider: model.provider ?? existing?.provider,
|
||||
}
|
||||
if (model.id && model.id !== modelID) {
|
||||
realIdByKey.set(`${providerID}/${modelID}`, model.id)
|
||||
},
|
||||
cost: {
|
||||
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
|
||||
output: model?.cost?.output ?? existingModel?.cost?.output ?? 0,
|
||||
cache: {
|
||||
read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0,
|
||||
write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
|
||||
},
|
||||
},
|
||||
options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}),
|
||||
limit: {
|
||||
context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
|
||||
output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
|
||||
},
|
||||
headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
|
||||
}
|
||||
parsed.models[modelID] = parsedModel
|
||||
}
|
||||
|
||||
database[providerID] = parsed
|
||||
}
|
||||
|
||||
// load env
|
||||
const env = Env.all()
|
||||
for (const [providerID, provider] of Object.entries(database)) {
|
||||
if (disabled.has(providerID)) continue
|
||||
const apiKey = provider.env.map((item) => process.env[item]).at(0)
|
||||
const apiKey = provider.env.map((item) => env[item]).find(Boolean)
|
||||
if (!apiKey) continue
|
||||
mergeProvider(
|
||||
providerID,
|
||||
// only include apiKey if there's only one potential option
|
||||
provider.env.length === 1 ? { apiKey } : {},
|
||||
"env",
|
||||
)
|
||||
mergeProvider(providerID, {
|
||||
source: "env",
|
||||
key: provider.env.length === 1 ? apiKey : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// load apikeys
|
||||
for (const [providerID, provider] of Object.entries(await Auth.all())) {
|
||||
if (disabled.has(providerID)) continue
|
||||
if (provider.type === "api") {
|
||||
mergeProvider(providerID, { apiKey: provider.key }, "api")
|
||||
mergeProvider(providerID, {
|
||||
source: "api",
|
||||
key: provider.key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +618,10 @@ export namespace Provider {
|
||||
// Load for the main provider if auth exists
|
||||
if (auth) {
|
||||
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
|
||||
mergeProvider(plugin.auth.provider, options ?? {}, "custom")
|
||||
mergeProvider(plugin.auth.provider, {
|
||||
source: "custom",
|
||||
options: options,
|
||||
})
|
||||
}
|
||||
|
||||
// If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
|
||||
@@ -490,7 +634,10 @@ export namespace Provider {
|
||||
() => Auth.get(enterpriseProviderID) as any,
|
||||
database[enterpriseProviderID],
|
||||
)
|
||||
mergeProvider(enterpriseProviderID, enterpriseOptions ?? {}, "custom")
|
||||
mergeProvider(enterpriseProviderID, {
|
||||
source: "custom",
|
||||
options: enterpriseOptions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,13 +647,21 @@ export namespace Provider {
|
||||
if (disabled.has(providerID)) continue
|
||||
const result = await fn(database[providerID])
|
||||
if (result && (result.autoload || providers[providerID])) {
|
||||
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
|
||||
if (result.getModel) modelLoaders[providerID] = result.getModel
|
||||
mergeProvider(providerID, {
|
||||
source: "custom",
|
||||
options: result.options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// load config
|
||||
for (const [providerID, provider] of configProviders) {
|
||||
mergeProvider(providerID, provider.options ?? {}, "config")
|
||||
const partial: Partial<Info> = { source: "config" }
|
||||
if (provider.env) partial.env = provider.env
|
||||
if (provider.name) partial.name = provider.name
|
||||
if (provider.options) partial.options = provider.options
|
||||
mergeProvider(providerID, partial)
|
||||
}
|
||||
|
||||
for (const [providerID, provider] of Object.entries(providers)) {
|
||||
@@ -515,50 +670,44 @@ export namespace Provider {
|
||||
continue
|
||||
}
|
||||
|
||||
if (providerID === "github-copilot") {
|
||||
provider.info.npm = "@ai-sdk/github-copilot"
|
||||
if (providerID === "github-copilot" || providerID === "github-copilot-enterprise") {
|
||||
provider.models = mapValues(provider.models, (model) => ({
|
||||
...model,
|
||||
api: {
|
||||
...model.api,
|
||||
npm: "@ai-sdk/github-copilot",
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const configProvider = config.provider?.[providerID]
|
||||
const filteredModels = Object.fromEntries(
|
||||
Object.entries(provider.info.models)
|
||||
// Filter out blacklisted models
|
||||
.filter(
|
||||
([modelID]) =>
|
||||
modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
|
||||
)
|
||||
// Filter out experimental models
|
||||
.filter(
|
||||
([, model]) =>
|
||||
((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
|
||||
model.status !== "deprecated",
|
||||
)
|
||||
// Filter by provider's whitelist/blacklist from config
|
||||
.filter(([modelID]) => {
|
||||
if (!configProvider) return true
|
||||
|
||||
return (
|
||||
(!configProvider.blacklist || !configProvider.blacklist.includes(modelID)) &&
|
||||
(!configProvider.whitelist || configProvider.whitelist.includes(modelID))
|
||||
)
|
||||
}),
|
||||
)
|
||||
for (const [modelID, model] of Object.entries(provider.models)) {
|
||||
model.api.id = model.api.id ?? model.id ?? modelID
|
||||
if (modelID === "gpt-5-chat-latest" || (providerID === "openrouter" && modelID === "openai/gpt-5-chat"))
|
||||
delete provider.models[modelID]
|
||||
if ((model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) || model.status === "deprecated")
|
||||
delete provider.models[modelID]
|
||||
if (
|
||||
(configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
|
||||
(configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
|
||||
)
|
||||
delete provider.models[modelID]
|
||||
}
|
||||
|
||||
provider.info.models = filteredModels
|
||||
|
||||
if (Object.keys(provider.info.models).length === 0) {
|
||||
if (Object.keys(provider.models).length === 0) {
|
||||
delete providers[providerID]
|
||||
continue
|
||||
}
|
||||
|
||||
log.info("found", { providerID, npm: provider.info.npm })
|
||||
log.info("found", { providerID })
|
||||
}
|
||||
|
||||
return {
|
||||
models,
|
||||
models: languages,
|
||||
providers,
|
||||
sdk,
|
||||
realIdByKey,
|
||||
modelLoaders,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -566,19 +715,28 @@ export namespace Provider {
|
||||
return state().then((state) => state.providers)
|
||||
}
|
||||
|
||||
async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model) {
|
||||
return (async () => {
|
||||
async function getSDK(model: Model) {
|
||||
try {
|
||||
using _ = log.time("getSDK", {
|
||||
providerID: provider.id,
|
||||
providerID: model.providerID,
|
||||
})
|
||||
const s = await state()
|
||||
const pkg = model.provider?.npm ?? provider.npm ?? provider.id
|
||||
const options = { ...s.providers[provider.id]?.options }
|
||||
if (pkg.includes("@ai-sdk/openai-compatible") && options["includeUsage"] === undefined) {
|
||||
const provider = s.providers[model.providerID]
|
||||
const options = { ...provider.options }
|
||||
|
||||
if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
|
||||
options["includeUsage"] = true
|
||||
}
|
||||
|
||||
const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
|
||||
if (!options["baseURL"]) options["baseURL"] = model.api.url
|
||||
if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
|
||||
if (model.headers)
|
||||
options["headers"] = {
|
||||
...options["headers"],
|
||||
...model.headers,
|
||||
}
|
||||
|
||||
const key = Bun.hash.xxHash32(JSON.stringify({ npm: model.api.npm, options }))
|
||||
const existing = s.sdk.get(key)
|
||||
if (existing) return existing
|
||||
|
||||
@@ -607,12 +765,13 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
// Special case: google-vertex-anthropic uses a subpath import
|
||||
const bundledKey = provider.id === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : pkg
|
||||
const bundledKey =
|
||||
model.providerID === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : model.api.npm
|
||||
const bundledFn = BUNDLED_PROVIDERS[bundledKey]
|
||||
if (bundledFn) {
|
||||
log.info("using bundled provider", { providerID: provider.id, pkg: bundledKey })
|
||||
log.info("using bundled provider", { providerID: model.providerID, pkg: bundledKey })
|
||||
const loaded = bundledFn({
|
||||
name: provider.id,
|
||||
name: model.providerID,
|
||||
...options,
|
||||
})
|
||||
s.sdk.set(key, loaded)
|
||||
@@ -620,25 +779,25 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
let installedPath: string
|
||||
if (!pkg.startsWith("file://")) {
|
||||
installedPath = await BunProc.install(pkg, "latest")
|
||||
if (!model.api.npm.startsWith("file://")) {
|
||||
installedPath = await BunProc.install(model.api.npm, "latest")
|
||||
} else {
|
||||
log.info("loading local provider", { pkg })
|
||||
installedPath = pkg
|
||||
log.info("loading local provider", { pkg: model.api.npm })
|
||||
installedPath = model.api.npm
|
||||
}
|
||||
|
||||
const mod = await import(installedPath)
|
||||
|
||||
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
||||
const loaded = fn({
|
||||
name: provider.id,
|
||||
const loaded = await fn({
|
||||
name: model.providerID,
|
||||
...options,
|
||||
})
|
||||
s.sdk.set(key, loaded)
|
||||
return loaded as SDK
|
||||
})().catch((e) => {
|
||||
throw new InitError({ providerID: provider.id }, { cause: e })
|
||||
})
|
||||
} catch (e) {
|
||||
throw new InitError({ providerID: model.providerID }, { cause: e })
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProvider(providerID: string) {
|
||||
@@ -646,15 +805,7 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
export async function getModel(providerID: string, modelID: string) {
|
||||
const key = `${providerID}/${modelID}`
|
||||
const s = await state()
|
||||
if (s.models.has(key)) return s.models.get(key)!
|
||||
|
||||
log.info("getModel", {
|
||||
providerID,
|
||||
modelID,
|
||||
})
|
||||
|
||||
const provider = s.providers[providerID]
|
||||
if (!provider) {
|
||||
const availableProviders = Object.keys(s.providers)
|
||||
@@ -663,43 +814,36 @@ export namespace Provider {
|
||||
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||
}
|
||||
|
||||
const info = provider.info.models[modelID]
|
||||
const info = provider.models[modelID]
|
||||
if (!info) {
|
||||
const availableModels = Object.keys(provider.info.models)
|
||||
const availableModels = Object.keys(provider.models)
|
||||
const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 })
|
||||
const suggestions = matches.map((m) => m.target)
|
||||
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
const sdk = await getSDK(provider.info, info)
|
||||
export async function getLanguage(model: Model) {
|
||||
const s = await state()
|
||||
const key = `${model.providerID}/${model.id}`
|
||||
if (s.models.has(key)) return s.models.get(key)!
|
||||
|
||||
const provider = s.providers[model.providerID]
|
||||
const sdk = await getSDK(model)
|
||||
|
||||
try {
|
||||
const keyReal = `${providerID}/${modelID}`
|
||||
const realID = s.realIdByKey.get(keyReal) ?? info.id
|
||||
const language = provider.getModel
|
||||
? await provider.getModel(sdk, realID, provider.options)
|
||||
: sdk.languageModel(realID)
|
||||
log.info("found", { providerID, modelID })
|
||||
s.models.set(key, {
|
||||
providerID,
|
||||
modelID,
|
||||
info,
|
||||
language,
|
||||
npm: info.provider?.npm ?? provider.info.npm,
|
||||
})
|
||||
return {
|
||||
modelID,
|
||||
providerID,
|
||||
info,
|
||||
language,
|
||||
npm: info.provider?.npm ?? provider.info.npm,
|
||||
}
|
||||
const language = s.modelLoaders[model.providerID]
|
||||
? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
|
||||
: sdk.languageModel(model.api.id)
|
||||
s.models.set(key, language)
|
||||
return language
|
||||
} catch (e) {
|
||||
if (e instanceof NoSuchModelError)
|
||||
throw new ModelNotFoundError(
|
||||
{
|
||||
modelID: modelID,
|
||||
providerID,
|
||||
modelID: model.id,
|
||||
providerID: model.providerID,
|
||||
},
|
||||
{ cause: e },
|
||||
)
|
||||
@@ -712,7 +856,7 @@ export namespace Provider {
|
||||
const provider = s.providers[providerID]
|
||||
if (!provider) return undefined
|
||||
for (const item of query) {
|
||||
for (const modelID of Object.keys(provider.info.models)) {
|
||||
for (const modelID of Object.keys(provider.models)) {
|
||||
if (modelID.includes(item))
|
||||
return {
|
||||
providerID,
|
||||
@@ -748,7 +892,7 @@ export namespace Provider {
|
||||
priority = ["gpt-5-nano"]
|
||||
}
|
||||
for (const item of priority) {
|
||||
for (const model of Object.keys(provider.info.models)) {
|
||||
for (const model of Object.keys(provider.models)) {
|
||||
if (model.includes(item)) return getModel(providerID, model)
|
||||
}
|
||||
}
|
||||
@@ -756,7 +900,7 @@ export namespace Provider {
|
||||
|
||||
// Check if opencode provider is available before using it
|
||||
const opencodeProvider = await state().then((state) => state.providers["opencode"])
|
||||
if (opencodeProvider && opencodeProvider.info.models["gpt-5-nano"]) {
|
||||
if (opencodeProvider && opencodeProvider.models["gpt-5-nano"]) {
|
||||
return getModel("opencode", "gpt-5-nano")
|
||||
}
|
||||
|
||||
@@ -764,7 +908,7 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
||||
export function sort(models: ModelsDev.Model[]) {
|
||||
export function sort(models: Model[]) {
|
||||
return sortBy(
|
||||
models,
|
||||
[(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
|
||||
@@ -779,12 +923,12 @@ export namespace Provider {
|
||||
|
||||
const provider = await list()
|
||||
.then((val) => Object.values(val))
|
||||
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
|
||||
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)))
|
||||
if (!provider) throw new Error("no providers found")
|
||||
const [model] = sort(Object.values(provider.info.models))
|
||||
const [model] = sort(Object.values(provider.models))
|
||||
if (!model) throw new Error("no models found")
|
||||
return {
|
||||
providerID: provider.info.id,
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { APICallError, ModelMessage } from "ai"
|
||||
import { unique } from "remeda"
|
||||
import type { JSONSchema } from "zod/v4/core"
|
||||
import type { Provider } from "./provider"
|
||||
|
||||
export namespace ProviderTransform {
|
||||
function normalizeMessages(msgs: ModelMessage[], providerID: string, modelID: string): ModelMessage[] {
|
||||
if (modelID.includes("claude")) {
|
||||
function normalizeMessages(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
|
||||
if (model.api.id.includes("claude")) {
|
||||
return msgs.map((msg) => {
|
||||
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.map((part) => {
|
||||
@@ -20,7 +21,7 @@ export namespace ProviderTransform {
|
||||
return msg
|
||||
})
|
||||
}
|
||||
if (providerID === "mistral" || modelID.toLowerCase().includes("mistral")) {
|
||||
if (model.providerID === "mistral" || model.api.id.toLowerCase().includes("mistral")) {
|
||||
const result: ModelMessage[] = []
|
||||
for (let i = 0; i < msgs.length; i++) {
|
||||
const msg = msgs[i]
|
||||
@@ -62,6 +63,46 @@ export namespace ProviderTransform {
|
||||
return result
|
||||
}
|
||||
|
||||
// DeepSeek: Handle reasoning_content for tool call continuations
|
||||
// - With tool calls: Include reasoning_content in providerOptions so model can continue reasoning
|
||||
// - Without tool calls: Strip reasoning (new turn doesn't need previous reasoning)
|
||||
// See: https://api-docs.deepseek.com/guides/thinking_mode
|
||||
if (model.providerID === "deepseek" || model.api.id.toLowerCase().includes("deepseek")) {
|
||||
return msgs.map((msg) => {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
|
||||
const hasToolCalls = msg.content.some((part: any) => part.type === "tool-call")
|
||||
const reasoningText = reasoningParts.map((part: any) => part.text).join("")
|
||||
|
||||
// Filter out reasoning parts from content
|
||||
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
|
||||
|
||||
// If this message has tool calls and reasoning, include reasoning_content
|
||||
// so DeepSeek can continue reasoning after tool execution
|
||||
if (hasToolCalls && reasoningText) {
|
||||
return {
|
||||
...msg,
|
||||
content: filteredContent,
|
||||
providerOptions: {
|
||||
...msg.providerOptions,
|
||||
openaiCompatible: {
|
||||
...(msg.providerOptions as any)?.openaiCompatible,
|
||||
reasoning_content: reasoningText,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// For final answers (no tool calls), just strip reasoning
|
||||
return {
|
||||
...msg,
|
||||
content: filteredContent,
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
@@ -107,67 +148,68 @@ export namespace ProviderTransform {
|
||||
return msgs
|
||||
}
|
||||
|
||||
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
|
||||
msgs = normalizeMessages(msgs, providerID, modelID)
|
||||
if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
|
||||
msgs = applyCaching(msgs, providerID)
|
||||
export function message(msgs: ModelMessage[], model: Provider.Model) {
|
||||
msgs = normalizeMessages(msgs, model)
|
||||
if (model.providerID === "anthropic" || model.api.id.includes("anthropic") || model.api.id.includes("claude")) {
|
||||
msgs = applyCaching(msgs, model.providerID)
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
export function temperature(_providerID: string, modelID: string) {
|
||||
if (modelID.toLowerCase().includes("qwen")) return 0.55
|
||||
if (modelID.toLowerCase().includes("claude")) return undefined
|
||||
if (modelID.toLowerCase().includes("gemini-3-pro")) return 1.0
|
||||
export function temperature(model: Provider.Model) {
|
||||
if (model.api.id.toLowerCase().includes("qwen")) return 0.55
|
||||
if (model.api.id.toLowerCase().includes("claude")) return undefined
|
||||
if (model.api.id.toLowerCase().includes("gemini-3-pro")) return 1.0
|
||||
return 0
|
||||
}
|
||||
|
||||
export function topP(_providerID: string, modelID: string) {
|
||||
if (modelID.toLowerCase().includes("qwen")) return 1
|
||||
export function topP(model: Provider.Model) {
|
||||
if (model.api.id.toLowerCase().includes("qwen")) return 1
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function options(
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
npm: string,
|
||||
model: Provider.Model,
|
||||
sessionID: string,
|
||||
providerOptions?: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
// switch to providerID later, for now use this
|
||||
if (npm === "@openrouter/ai-sdk-provider") {
|
||||
if (model.api.npm === "@openrouter/ai-sdk-provider") {
|
||||
result["usage"] = {
|
||||
include: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (providerID === "openai" || providerOptions?.setCacheKey) {
|
||||
if (model.providerID === "openai" || providerOptions?.setCacheKey) {
|
||||
result["promptCacheKey"] = sessionID
|
||||
}
|
||||
|
||||
if (providerID === "google" || (providerID.startsWith("opencode") && modelID.includes("gemini-3"))) {
|
||||
if (
|
||||
model.providerID === "google" ||
|
||||
(model.providerID.startsWith("opencode") && model.api.id.includes("gemini-3"))
|
||||
) {
|
||||
result["thinkingConfig"] = {
|
||||
includeThoughts: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
|
||||
if (modelID.includes("codex")) {
|
||||
if (model.api.id.includes("gpt-5") && !model.api.id.includes("gpt-5-chat")) {
|
||||
if (model.providerID.includes("codex")) {
|
||||
result["store"] = false
|
||||
}
|
||||
|
||||
if (!modelID.includes("codex") && !modelID.includes("gpt-5-pro")) {
|
||||
if (!model.api.id.includes("codex") && !model.api.id.includes("gpt-5-pro")) {
|
||||
result["reasoningEffort"] = "medium"
|
||||
}
|
||||
|
||||
if (modelID.endsWith("gpt-5.1") && providerID !== "azure") {
|
||||
if (model.api.id.endsWith("gpt-5.1") && model.providerID !== "azure") {
|
||||
result["textVerbosity"] = "low"
|
||||
}
|
||||
|
||||
if (providerID.startsWith("opencode")) {
|
||||
if (model.providerID.startsWith("opencode")) {
|
||||
result["promptCacheKey"] = sessionID
|
||||
result["include"] = ["reasoning.encrypted_content"]
|
||||
result["reasoningSummary"] = "auto"
|
||||
@@ -176,17 +218,17 @@ export namespace ProviderTransform {
|
||||
return result
|
||||
}
|
||||
|
||||
export function smallOptions(input: { providerID: string; modelID: string }) {
|
||||
export function smallOptions(model: Provider.Model) {
|
||||
const options: Record<string, any> = {}
|
||||
|
||||
if (input.providerID === "openai" || input.modelID.includes("gpt-5")) {
|
||||
if (input.modelID.includes("5.1")) {
|
||||
if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
|
||||
if (model.api.id.includes("5.1")) {
|
||||
options["reasoningEffort"] = "low"
|
||||
} else {
|
||||
options["reasoningEffort"] = "minimal"
|
||||
}
|
||||
}
|
||||
if (input.providerID === "google") {
|
||||
if (model.providerID === "google") {
|
||||
options["thinkingConfig"] = {
|
||||
thinkingBudget: 0,
|
||||
}
|
||||
@@ -254,7 +296,7 @@ export namespace ProviderTransform {
|
||||
return standardLimit
|
||||
}
|
||||
|
||||
export function schema(providerID: string, modelID: string, schema: JSONSchema.BaseSchema) {
|
||||
export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema) {
|
||||
/*
|
||||
if (["openai", "azure"].includes(providerID)) {
|
||||
if (schema.type === "object" && schema.properties) {
|
||||
@@ -274,7 +316,7 @@ export namespace ProviderTransform {
|
||||
*/
|
||||
|
||||
// Convert integer enums to string enums for Google/Gemini
|
||||
if (providerID === "google" || modelID.includes("gemini")) {
|
||||
if (model.providerID === "google" || model.api.id.includes("gemini")) {
|
||||
const sanitizeGemini = (obj: any): any => {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj
|
||||
|
||||
@@ -8,7 +8,7 @@ import { proxy } from "hono/proxy"
|
||||
import { Session } from "../session"
|
||||
import z from "zod"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { mapValues } from "remeda"
|
||||
import { mapValues, pipe } from "remeda"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { ModelsDev } from "../provider/models"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
@@ -296,8 +296,8 @@ export namespace Server {
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { provider, model } = c.req.valid("query")
|
||||
const tools = await ToolRegistry.tools(provider, model)
|
||||
const { provider } = c.req.valid("query")
|
||||
const tools = await ToolRegistry.tools(provider)
|
||||
return c.json(
|
||||
tools.map((t) => ({
|
||||
id: t.id,
|
||||
@@ -1025,7 +1025,7 @@ export namespace Server {
|
||||
async (c) => {
|
||||
c.status(204)
|
||||
c.header("Content-Type", "application/json")
|
||||
return stream(c, async (stream) => {
|
||||
return stream(c, async () => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
SessionPrompt.prompt({ ...body, sessionID })
|
||||
@@ -1231,7 +1231,7 @@ export namespace Server {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
providers: ModelsDev.Provider.array(),
|
||||
providers: Provider.Info.array(),
|
||||
default: z.record(z.string(), z.string()),
|
||||
}),
|
||||
),
|
||||
@@ -1242,7 +1242,7 @@ export namespace Server {
|
||||
}),
|
||||
async (c) => {
|
||||
using _ = log.time("providers")
|
||||
const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
|
||||
const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
|
||||
return c.json({
|
||||
providers: Object.values(providers),
|
||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||
@@ -1272,7 +1272,10 @@ export namespace Server {
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const providers = await ModelsDev.get()
|
||||
const providers = pipe(
|
||||
await ModelsDev.get(),
|
||||
mapValues((x) => Provider.fromModelsDevProvider(x)),
|
||||
)
|
||||
const connected = await Provider.list().then((x) => Object.keys(x))
|
||||
return c.json({
|
||||
all: Object.values(providers),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { streamText, wrapLanguageModel, type ModelMessage } from "ai"
|
||||
import { wrapLanguageModel, type ModelMessage } from "ai"
|
||||
import { Session } from "."
|
||||
import { Identifier } from "../id/id"
|
||||
import { Instance } from "../project/instance"
|
||||
@@ -7,7 +7,6 @@ import { MessageV2 } from "./message-v2"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { Bus } from "../bus"
|
||||
import z from "zod"
|
||||
import type { ModelsDev } from "../provider/models"
|
||||
import { SessionPrompt } from "./prompt"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Token } from "../util/token"
|
||||
@@ -29,7 +28,7 @@ export namespace SessionCompaction {
|
||||
),
|
||||
}
|
||||
|
||||
export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: ModelsDev.Model }) {
|
||||
export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) {
|
||||
if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) return false
|
||||
const context = input.model.limit.context
|
||||
if (context === 0) return false
|
||||
@@ -98,6 +97,7 @@ export namespace SessionCompaction {
|
||||
auto: boolean
|
||||
}) {
|
||||
const model = await Provider.getModel(input.model.providerID, input.model.modelID)
|
||||
const language = await Provider.getLanguage(model)
|
||||
const system = [...SystemPrompt.compaction(model.providerID)]
|
||||
const msg = (await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
@@ -126,79 +126,72 @@ export namespace SessionCompaction {
|
||||
const processor = SessionProcessor.create({
|
||||
assistantMessage: msg,
|
||||
sessionID: input.sessionID,
|
||||
providerID: input.model.providerID,
|
||||
model: model.info,
|
||||
model: model,
|
||||
abort: input.abort,
|
||||
})
|
||||
const result = await processor.process(() =>
|
||||
streamText({
|
||||
onError(error) {
|
||||
log.error("stream error", {
|
||||
error,
|
||||
})
|
||||
},
|
||||
// set to 0, we handle loop
|
||||
maxRetries: 0,
|
||||
providerOptions: ProviderTransform.providerOptions(
|
||||
model.npm,
|
||||
model.providerID,
|
||||
pipe(
|
||||
{},
|
||||
mergeDeep(ProviderTransform.options(model.providerID, model.modelID, model.npm ?? "", input.sessionID)),
|
||||
mergeDeep(model.info.options),
|
||||
),
|
||||
const result = await processor.process({
|
||||
onError(error) {
|
||||
log.error("stream error", {
|
||||
error,
|
||||
})
|
||||
},
|
||||
// set to 0, we handle loop
|
||||
maxRetries: 0,
|
||||
providerOptions: ProviderTransform.providerOptions(
|
||||
model.api.npm,
|
||||
model.providerID,
|
||||
pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
|
||||
),
|
||||
headers: model.headers,
|
||||
abortSignal: input.abort,
|
||||
tools: model.capabilities.toolcall ? {} : undefined,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
headers: model.info.headers,
|
||||
abortSignal: input.abort,
|
||||
tools: model.info.tool_call ? {} : undefined,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
...MessageV2.toModelMessage(
|
||||
input.messages.filter((m) => {
|
||||
if (m.info.role !== "assistant" || m.info.error === undefined) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
MessageV2.AbortedError.isInstance(m.info.error) &&
|
||||
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
...MessageV2.toModelMessage(
|
||||
input.messages.filter((m) => {
|
||||
if (m.info.role !== "assistant" || m.info.error === undefined) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
MessageV2.AbortedError.isInstance(m.info.error) &&
|
||||
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}),
|
||||
),
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
model: wrapLanguageModel({
|
||||
model: model.language,
|
||||
middleware: [
|
||||
return false
|
||||
}),
|
||||
),
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
// @ts-expect-error
|
||||
args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
type: "text",
|
||||
text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
model: wrapLanguageModel({
|
||||
model: language,
|
||||
middleware: [
|
||||
{
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
// @ts-expect-error
|
||||
args.params.prompt = ProviderTransform.message(args.params.prompt, model)
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
if (result === "continue" && input.auto) {
|
||||
const continueMsg = await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
|
||||
@@ -6,8 +6,7 @@ import { Config } from "../config/config"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Installation } from "../installation"
|
||||
import type { ModelsDev } from "../provider/models"
|
||||
import { Share } from "../share/share"
|
||||
|
||||
import { Storage } from "../storage/storage"
|
||||
import { Log } from "../util/log"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
@@ -16,7 +15,8 @@ import { SessionPrompt } from "./prompt"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Command } from "../command"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
import type { Provider } from "@/provider/provider"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -223,6 +223,7 @@ export namespace Session {
|
||||
}
|
||||
|
||||
if (cfg.enterprise?.url) {
|
||||
const { ShareNext } = await import("@/share/share-next")
|
||||
const share = await ShareNext.create(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = {
|
||||
@@ -233,6 +234,7 @@ export namespace Session {
|
||||
|
||||
const session = await get(id)
|
||||
if (session.share) return session.share
|
||||
const { Share } = await import("../share/share")
|
||||
const share = await Share.create(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = {
|
||||
@@ -253,6 +255,7 @@ export namespace Session {
|
||||
export const unshare = fn(Identifier.schema("session"), async (id) => {
|
||||
const cfg = await Config.get()
|
||||
if (cfg.enterprise?.url) {
|
||||
const { ShareNext } = await import("@/share/share-next")
|
||||
await ShareNext.remove(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = undefined
|
||||
@@ -264,6 +267,7 @@ export namespace Session {
|
||||
await update(id, (draft) => {
|
||||
draft.share = undefined
|
||||
})
|
||||
const { Share } = await import("../share/share")
|
||||
await Share.remove(id, share.secret)
|
||||
})
|
||||
|
||||
@@ -389,7 +393,7 @@ export namespace Session {
|
||||
|
||||
export const getUsage = fn(
|
||||
z.object({
|
||||
model: z.custom<ModelsDev.Model>(),
|
||||
model: z.custom<Provider.Model>(),
|
||||
usage: z.custom<LanguageModelUsage>(),
|
||||
metadata: z.custom<ProviderMetadata>().optional(),
|
||||
}),
|
||||
@@ -420,16 +424,16 @@ export namespace Session {
|
||||
}
|
||||
|
||||
const costInfo =
|
||||
input.model.cost?.context_over_200k && tokens.input + tokens.cache.read > 200_000
|
||||
? input.model.cost.context_over_200k
|
||||
input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
|
||||
? input.model.cost.experimentalOver200K
|
||||
: input.model.cost
|
||||
return {
|
||||
cost: 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.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))
|
||||
// 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))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ModelsDev } from "@/provider/models"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { type StreamTextResult, type Tool as AITool, APICallError } from "ai"
|
||||
import { streamText } from "ai"
|
||||
import { Log } from "@/util/log"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { Session } from "."
|
||||
@@ -11,6 +10,7 @@ import { SessionSummary } from "./summary"
|
||||
import { Bus } from "@/bus"
|
||||
import { SessionRetry } from "./retry"
|
||||
import { SessionStatus } from "./status"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
|
||||
export namespace SessionProcessor {
|
||||
const DOOM_LOOP_THRESHOLD = 3
|
||||
@@ -19,11 +19,19 @@ export namespace SessionProcessor {
|
||||
export type Info = Awaited<ReturnType<typeof create>>
|
||||
export type Result = Awaited<ReturnType<Info["process"]>>
|
||||
|
||||
export type StreamInput = Parameters<typeof streamText>[0]
|
||||
|
||||
export type TBD = {
|
||||
model: {
|
||||
modelID: string
|
||||
providerID: string
|
||||
}
|
||||
}
|
||||
|
||||
export function create(input: {
|
||||
assistantMessage: MessageV2.Assistant
|
||||
sessionID: string
|
||||
providerID: string
|
||||
model: ModelsDev.Model
|
||||
model: Provider.Model
|
||||
abort: AbortSignal
|
||||
}) {
|
||||
const toolcalls: Record<string, MessageV2.ToolPart> = {}
|
||||
@@ -38,13 +46,13 @@ export namespace SessionProcessor {
|
||||
partFromToolCall(toolCallID: string) {
|
||||
return toolcalls[toolCallID]
|
||||
},
|
||||
async process(fn: () => StreamTextResult<Record<string, AITool>, never>) {
|
||||
async process(streamInput: StreamInput) {
|
||||
log.info("process")
|
||||
while (true) {
|
||||
try {
|
||||
let currentText: MessageV2.TextPart | undefined
|
||||
let reasoningMap: Record<string, MessageV2.ReasoningPart> = {}
|
||||
const stream = fn()
|
||||
const stream = streamText(streamInput)
|
||||
|
||||
for await (const value of stream.fullStream) {
|
||||
input.abort.throwIfAborted()
|
||||
@@ -328,11 +336,12 @@ export namespace SessionProcessor {
|
||||
continue
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
log.error("process", {
|
||||
error: e,
|
||||
stack: JSON.stringify(e.stack),
|
||||
})
|
||||
const error = MessageV2.fromError(e, { providerID: input.providerID })
|
||||
const error = MessageV2.fromError(e, { providerID: input.model.providerID })
|
||||
const retry = SessionRetry.retryable(error)
|
||||
if (retry !== undefined) {
|
||||
attempt++
|
||||
|
||||
@@ -11,7 +11,6 @@ import { Agent } from "../agent/agent"
|
||||
import { Provider } from "../provider/provider"
|
||||
import {
|
||||
generateText,
|
||||
streamText,
|
||||
type ModelMessage,
|
||||
type Tool as AITool,
|
||||
tool,
|
||||
@@ -288,6 +287,7 @@ export namespace SessionPrompt {
|
||||
})
|
||||
|
||||
const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
|
||||
const language = await Provider.getLanguage(model)
|
||||
const task = tasks.pop()
|
||||
|
||||
// pending subtask
|
||||
@@ -311,7 +311,7 @@ export namespace SessionPrompt {
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: model.modelID,
|
||||
modelID: model.id,
|
||||
providerID: model.providerID,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -408,7 +408,7 @@ export namespace SessionPrompt {
|
||||
agent: lastUser.agent,
|
||||
model: {
|
||||
providerID: model.providerID,
|
||||
modelID: model.modelID,
|
||||
modelID: model.id,
|
||||
},
|
||||
sessionID,
|
||||
auto: task.auto,
|
||||
@@ -421,7 +421,7 @@ export namespace SessionPrompt {
|
||||
if (
|
||||
lastFinished &&
|
||||
lastFinished.summary !== true &&
|
||||
SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model: model.info })
|
||||
SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })
|
||||
) {
|
||||
await SessionCompaction.create({
|
||||
sessionID,
|
||||
@@ -455,7 +455,7 @@ export namespace SessionPrompt {
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: model.modelID,
|
||||
modelID: model.id,
|
||||
providerID: model.providerID,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -463,20 +463,18 @@ export namespace SessionPrompt {
|
||||
sessionID,
|
||||
})) as MessageV2.Assistant,
|
||||
sessionID: sessionID,
|
||||
model: model.info,
|
||||
providerID: model.providerID,
|
||||
model,
|
||||
abort,
|
||||
})
|
||||
const system = await resolveSystemPrompt({
|
||||
providerID: model.providerID,
|
||||
modelID: model.info.id,
|
||||
model,
|
||||
agent,
|
||||
system: lastUser.system,
|
||||
})
|
||||
const tools = await resolveTools({
|
||||
agent,
|
||||
sessionID,
|
||||
model: lastUser.model,
|
||||
model,
|
||||
tools: lastUser.tools,
|
||||
processor,
|
||||
})
|
||||
@@ -486,21 +484,19 @@ export namespace SessionPrompt {
|
||||
{
|
||||
sessionID: sessionID,
|
||||
agent: lastUser.agent,
|
||||
model: model.info,
|
||||
model: model,
|
||||
provider,
|
||||
message: lastUser,
|
||||
},
|
||||
{
|
||||
temperature: model.info.temperature
|
||||
? (agent.temperature ?? ProviderTransform.temperature(model.providerID, model.modelID))
|
||||
temperature: model.capabilities.temperature
|
||||
? (agent.temperature ?? ProviderTransform.temperature(model))
|
||||
: undefined,
|
||||
topP: agent.topP ?? ProviderTransform.topP(model.providerID, model.modelID),
|
||||
topP: agent.topP ?? ProviderTransform.topP(model),
|
||||
options: pipe(
|
||||
{},
|
||||
mergeDeep(
|
||||
ProviderTransform.options(model.providerID, model.modelID, model.npm ?? "", sessionID, provider?.options),
|
||||
),
|
||||
mergeDeep(model.info.options),
|
||||
mergeDeep(ProviderTransform.options(model, sessionID, provider?.options)),
|
||||
mergeDeep(model.options),
|
||||
mergeDeep(agent.options),
|
||||
),
|
||||
},
|
||||
@@ -513,112 +509,111 @@ export namespace SessionPrompt {
|
||||
})
|
||||
}
|
||||
|
||||
const result = await processor.process(() =>
|
||||
streamText({
|
||||
onError(error) {
|
||||
log.error("stream error", {
|
||||
error,
|
||||
const result = await processor.process({
|
||||
onError(error) {
|
||||
log.error("stream error", {
|
||||
error,
|
||||
})
|
||||
},
|
||||
async experimental_repairToolCall(input) {
|
||||
const lower = input.toolCall.toolName.toLowerCase()
|
||||
if (lower !== input.toolCall.toolName && tools[lower]) {
|
||||
log.info("repairing tool call", {
|
||||
tool: input.toolCall.toolName,
|
||||
repaired: lower,
|
||||
})
|
||||
},
|
||||
async experimental_repairToolCall(input) {
|
||||
const lower = input.toolCall.toolName.toLowerCase()
|
||||
if (lower !== input.toolCall.toolName && tools[lower]) {
|
||||
log.info("repairing tool call", {
|
||||
tool: input.toolCall.toolName,
|
||||
repaired: lower,
|
||||
})
|
||||
return {
|
||||
...input.toolCall,
|
||||
toolName: lower,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...input.toolCall,
|
||||
input: JSON.stringify({
|
||||
tool: input.toolCall.toolName,
|
||||
error: input.error.message,
|
||||
}),
|
||||
toolName: "invalid",
|
||||
toolName: lower,
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
...(model.providerID.startsWith("opencode")
|
||||
? {
|
||||
"x-opencode-session": sessionID,
|
||||
"x-opencode-request": lastUser.id,
|
||||
}
|
||||
: undefined),
|
||||
...model.info.headers,
|
||||
},
|
||||
// set to 0, we handle loop
|
||||
maxRetries: 0,
|
||||
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
|
||||
maxOutputTokens: ProviderTransform.maxOutputTokens(
|
||||
model.providerID,
|
||||
params.options,
|
||||
model.info.limit.output,
|
||||
OUTPUT_TOKEN_MAX,
|
||||
}
|
||||
return {
|
||||
...input.toolCall,
|
||||
input: JSON.stringify({
|
||||
tool: input.toolCall.toolName,
|
||||
error: input.error.message,
|
||||
}),
|
||||
toolName: "invalid",
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
...(model.providerID.startsWith("opencode")
|
||||
? {
|
||||
"x-opencode-project": Instance.project.id,
|
||||
"x-opencode-session": sessionID,
|
||||
"x-opencode-request": lastUser.id,
|
||||
}
|
||||
: undefined),
|
||||
...model.headers,
|
||||
},
|
||||
// set to 0, we handle loop
|
||||
maxRetries: 0,
|
||||
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
|
||||
maxOutputTokens: ProviderTransform.maxOutputTokens(
|
||||
model.api.npm,
|
||||
params.options,
|
||||
model.limit.output,
|
||||
OUTPUT_TOKEN_MAX,
|
||||
),
|
||||
abortSignal: abort,
|
||||
providerOptions: ProviderTransform.providerOptions(model.api.npm, model.providerID, params.options),
|
||||
stopWhen: stepCountIs(1),
|
||||
temperature: params.temperature,
|
||||
topP: params.topP,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
abortSignal: abort,
|
||||
providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
|
||||
stopWhen: stepCountIs(1),
|
||||
temperature: params.temperature,
|
||||
topP: params.topP,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
...MessageV2.toModelMessage(
|
||||
msgs.filter((m) => {
|
||||
if (m.info.role !== "assistant" || m.info.error === undefined) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
MessageV2.AbortedError.isInstance(m.info.error) &&
|
||||
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
...MessageV2.toModelMessage(
|
||||
msgs.filter((m) => {
|
||||
if (m.info.role !== "assistant" || m.info.error === undefined) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
MessageV2.AbortedError.isInstance(m.info.error) &&
|
||||
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}),
|
||||
),
|
||||
],
|
||||
tools: model.info.tool_call === false ? undefined : tools,
|
||||
model: wrapLanguageModel({
|
||||
model: model.language,
|
||||
middleware: [
|
||||
{
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
// @ts-expect-error
|
||||
args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
|
||||
}
|
||||
// Transform tool schemas for provider compatibility
|
||||
if (args.params.tools && Array.isArray(args.params.tools)) {
|
||||
args.params.tools = args.params.tools.map((tool: any) => {
|
||||
// Tools at middleware level have inputSchema, not parameters
|
||||
if (tool.inputSchema && typeof tool.inputSchema === "object") {
|
||||
// Transform the inputSchema for provider compatibility
|
||||
return {
|
||||
...tool,
|
||||
inputSchema: ProviderTransform.schema(model.providerID, model.modelID, tool.inputSchema),
|
||||
}
|
||||
return false
|
||||
}),
|
||||
),
|
||||
],
|
||||
tools: model.capabilities.toolcall === false ? undefined : tools,
|
||||
model: wrapLanguageModel({
|
||||
model: language,
|
||||
middleware: [
|
||||
{
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
// @ts-expect-error - prompt types are compatible at runtime
|
||||
args.params.prompt = ProviderTransform.message(args.params.prompt, model)
|
||||
}
|
||||
// Transform tool schemas for provider compatibility
|
||||
if (args.params.tools && Array.isArray(args.params.tools)) {
|
||||
args.params.tools = args.params.tools.map((tool: any) => {
|
||||
// Tools at middleware level have inputSchema, not parameters
|
||||
if (tool.inputSchema && typeof tool.inputSchema === "object") {
|
||||
// Transform the inputSchema for provider compatibility
|
||||
return {
|
||||
...tool,
|
||||
inputSchema: ProviderTransform.schema(model, tool.inputSchema),
|
||||
}
|
||||
// If no inputSchema, return tool unchanged
|
||||
return tool
|
||||
})
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
}
|
||||
// If no inputSchema, return tool unchanged
|
||||
return tool
|
||||
})
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
if (result === "stop") break
|
||||
continue
|
||||
}
|
||||
@@ -641,18 +636,13 @@ export namespace SessionPrompt {
|
||||
return Provider.defaultModel()
|
||||
}
|
||||
|
||||
async function resolveSystemPrompt(input: {
|
||||
system?: string
|
||||
agent: Agent.Info
|
||||
providerID: string
|
||||
modelID: string
|
||||
}) {
|
||||
let system = SystemPrompt.header(input.providerID)
|
||||
async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) {
|
||||
let system = SystemPrompt.header(input.model.providerID)
|
||||
system.push(
|
||||
...(() => {
|
||||
if (input.system) return [input.system]
|
||||
if (input.agent.prompt) return [input.agent.prompt]
|
||||
return SystemPrompt.provider(input.modelID)
|
||||
return SystemPrompt.provider(input.model)
|
||||
})(),
|
||||
)
|
||||
system.push(...(await SystemPrompt.environment()))
|
||||
@@ -665,10 +655,7 @@ export namespace SessionPrompt {
|
||||
|
||||
async function resolveTools(input: {
|
||||
agent: Agent.Info
|
||||
model: {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
model: Provider.Model
|
||||
sessionID: string
|
||||
tools?: Record<string, boolean>
|
||||
processor: SessionProcessor.Info
|
||||
@@ -676,16 +663,12 @@ export namespace SessionPrompt {
|
||||
const tools: Record<string, AITool> = {}
|
||||
const enabledTools = pipe(
|
||||
input.agent.tools,
|
||||
mergeDeep(await ToolRegistry.enabled(input.model.providerID, input.model.modelID, input.agent)),
|
||||
mergeDeep(await ToolRegistry.enabled(input.agent)),
|
||||
mergeDeep(input.tools ?? {}),
|
||||
)
|
||||
for (const item of await ToolRegistry.tools(input.model.providerID, input.model.modelID)) {
|
||||
for (const item of await ToolRegistry.tools(input.model.providerID)) {
|
||||
if (Wildcard.all(item.id, enabledTools) === false) continue
|
||||
const schema = ProviderTransform.schema(
|
||||
input.model.providerID,
|
||||
input.model.modelID,
|
||||
z.toJSONSchema(item.parameters),
|
||||
)
|
||||
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
|
||||
tools[item.id] = tool({
|
||||
id: item.id as any,
|
||||
description: item.description,
|
||||
@@ -707,7 +690,7 @@ export namespace SessionPrompt {
|
||||
abort: options.abortSignal!,
|
||||
messageID: input.processor.message.id,
|
||||
callID: options.toolCallId,
|
||||
extra: input.model,
|
||||
extra: { model: input.model },
|
||||
agent: input.agent.name,
|
||||
metadata: async (val) => {
|
||||
const match = input.processor.partFromToolCall(options.toolCallId)
|
||||
@@ -924,12 +907,13 @@ export namespace SessionPrompt {
|
||||
|
||||
await ReadTool.init()
|
||||
.then(async (t) => {
|
||||
const model = await Provider.getModel(info.model.providerID, info.model.modelID)
|
||||
const result = await t.execute(args, {
|
||||
sessionID: input.sessionID,
|
||||
abort: new AbortController().signal,
|
||||
agent: input.agent!,
|
||||
messageID: info.id,
|
||||
extra: { bypassCwdCheck: true, ...info.model },
|
||||
extra: { bypassCwdCheck: true, model },
|
||||
metadata: async () => {},
|
||||
})
|
||||
pieces.push({
|
||||
@@ -1436,25 +1420,18 @@ export namespace SessionPrompt {
|
||||
if (!isFirst) return
|
||||
const small =
|
||||
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
|
||||
const language = await Provider.getLanguage(small)
|
||||
const provider = await Provider.getProvider(small.providerID)
|
||||
const options = pipe(
|
||||
{},
|
||||
mergeDeep(
|
||||
ProviderTransform.options(
|
||||
small.providerID,
|
||||
small.modelID,
|
||||
small.npm ?? "",
|
||||
input.session.id,
|
||||
provider?.options,
|
||||
),
|
||||
),
|
||||
mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, modelID: small.modelID })),
|
||||
mergeDeep(small.info.options),
|
||||
mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)),
|
||||
mergeDeep(ProviderTransform.smallOptions(small)),
|
||||
mergeDeep(small.options),
|
||||
)
|
||||
await generateText({
|
||||
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget
|
||||
maxOutputTokens: small.info.reasoning ? 3000 : 20,
|
||||
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
|
||||
maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
|
||||
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
|
||||
messages: [
|
||||
...SystemPrompt.title(small.providerID).map(
|
||||
(x): ModelMessage => ({
|
||||
@@ -1485,8 +1462,8 @@ export namespace SessionPrompt {
|
||||
},
|
||||
]),
|
||||
],
|
||||
headers: small.info.headers,
|
||||
model: small.language,
|
||||
headers: small.headers,
|
||||
model: language,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.text)
|
||||
@@ -1503,7 +1480,7 @@ export namespace SessionPrompt {
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to generate title", { error, model: small.info.id })
|
||||
log.error("failed to generate title", { error, model: small.id })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,9 @@ export namespace SessionRetry {
|
||||
if (json.type === "error" && json.error?.type === "too_many_requests") {
|
||||
return "Too Many Requests"
|
||||
}
|
||||
if (json.code === "Some resource has been exhausted") {
|
||||
return "Provider is overloaded"
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,19 +76,20 @@ export namespace SessionSummary {
|
||||
const small =
|
||||
(await Provider.getSmallModel(assistantMsg.providerID)) ??
|
||||
(await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID))
|
||||
const language = await Provider.getLanguage(small)
|
||||
|
||||
const options = pipe(
|
||||
{},
|
||||
mergeDeep(ProviderTransform.options(small.providerID, small.modelID, small.npm ?? "", assistantMsg.sessionID)),
|
||||
mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, modelID: small.modelID })),
|
||||
mergeDeep(small.info.options),
|
||||
mergeDeep(ProviderTransform.options(small, assistantMsg.sessionID)),
|
||||
mergeDeep(ProviderTransform.smallOptions(small)),
|
||||
mergeDeep(small.options),
|
||||
)
|
||||
|
||||
const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
|
||||
if (textPart && !userMsg.summary?.title) {
|
||||
const result = await generateText({
|
||||
maxOutputTokens: small.info.reasoning ? 1500 : 20,
|
||||
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
|
||||
maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
|
||||
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
|
||||
messages: [
|
||||
...SystemPrompt.title(small.providerID).map(
|
||||
(x): ModelMessage => ({
|
||||
@@ -106,8 +107,8 @@ export namespace SessionSummary {
|
||||
`,
|
||||
},
|
||||
],
|
||||
headers: small.info.headers,
|
||||
model: small.language,
|
||||
headers: small.headers,
|
||||
model: language,
|
||||
})
|
||||
log.info("title", { title: result.text })
|
||||
userMsg.summary.title = result.text
|
||||
@@ -132,9 +133,9 @@ export namespace SessionSummary {
|
||||
}
|
||||
}
|
||||
const result = await generateText({
|
||||
model: small.language,
|
||||
model: language,
|
||||
maxOutputTokens: 100,
|
||||
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
|
||||
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
|
||||
messages: [
|
||||
...SystemPrompt.summarize(small.providerID).map(
|
||||
(x): ModelMessage => ({
|
||||
@@ -148,7 +149,7 @@ export namespace SessionSummary {
|
||||
content: `Summarize the above conversation according to your system prompts.`,
|
||||
},
|
||||
],
|
||||
headers: small.info.headers,
|
||||
headers: small.headers,
|
||||
}).catch(() => {})
|
||||
if (result) summary = result.text
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
||||
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
|
||||
import PROMPT_TITLE from "./prompt/title.txt"
|
||||
import PROMPT_CODEX from "./prompt/codex.txt"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
|
||||
export namespace SystemPrompt {
|
||||
export function header(providerID: string) {
|
||||
@@ -24,12 +25,13 @@ export namespace SystemPrompt {
|
||||
return []
|
||||
}
|
||||
|
||||
export function provider(modelID: string) {
|
||||
if (modelID.includes("gpt-5")) return [PROMPT_CODEX]
|
||||
if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
|
||||
if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
|
||||
if (modelID.includes("claude")) return [PROMPT_ANTHROPIC]
|
||||
if (modelID.includes("polaris-alpha")) return [PROMPT_POLARIS]
|
||||
export function provider(model: Provider.Model) {
|
||||
if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX]
|
||||
if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3"))
|
||||
return [PROMPT_BEAST]
|
||||
if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
|
||||
if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
|
||||
if (model.api.id.includes("polaris-alpha")) return [PROMPT_POLARIS]
|
||||
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Bus } from "@/bus"
|
||||
import { Config } from "@/config/config"
|
||||
import { ulid } from "ulid"
|
||||
import type { ModelsDev } from "@/provider/models"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Session } from "@/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
@@ -36,7 +35,7 @@ export namespace ShareNext {
|
||||
type: "model",
|
||||
data: [
|
||||
await Provider.getModel(evt.properties.info.model.providerID, evt.properties.info.model.modelID).then(
|
||||
(m) => m.info,
|
||||
(m) => m,
|
||||
),
|
||||
],
|
||||
},
|
||||
@@ -105,7 +104,7 @@ export namespace ShareNext {
|
||||
}
|
||||
| {
|
||||
type: "model"
|
||||
data: ModelsDev.Model[]
|
||||
data: SDK.Model[]
|
||||
}
|
||||
|
||||
const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
|
||||
@@ -171,7 +170,7 @@ export namespace ShareNext {
|
||||
messages
|
||||
.filter((m) => m.info.role === "user")
|
||||
.map((m) => (m.info as SDK.UserMessage).model)
|
||||
.map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m.info)),
|
||||
.map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m)),
|
||||
)
|
||||
await sync(sessionID, [
|
||||
{
|
||||
|
||||
@@ -316,17 +316,24 @@ export const BashTool = Tool.define("bash", async () => {
|
||||
})
|
||||
})
|
||||
|
||||
let resultMetadata: String[] = ["<bash_metadata>"]
|
||||
|
||||
if (output.length > MAX_OUTPUT_LENGTH) {
|
||||
output = output.slice(0, MAX_OUTPUT_LENGTH)
|
||||
output += "\n\n(Output was truncated due to length limit)"
|
||||
resultMetadata.push(`Output exceeded length limit of ${MAX_OUTPUT_LENGTH} chars`)
|
||||
}
|
||||
|
||||
if (timedOut) {
|
||||
output += `\n\n(Command timed out after ${timeout} ms)`
|
||||
resultMetadata.push(`Command terminated after exceeding timeout ${timeout} ms`)
|
||||
}
|
||||
|
||||
if (aborted) {
|
||||
output += "\n\n(Command was aborted)"
|
||||
resultMetadata.push("Command aborted by user")
|
||||
}
|
||||
|
||||
if (resultMetadata.length > 1) {
|
||||
resultMetadata.push("</bash_metadata>")
|
||||
output += "\n\n" + resultMetadata.join("\n")
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -35,6 +35,8 @@ Usage notes:
|
||||
|
||||
# Committing changes with git
|
||||
|
||||
IMPORTANT: ONLY COMMIT IF THE USER ASKS YOU TO.
|
||||
|
||||
If and only if the user asks you to create a new git commit, follow these steps carefully:
|
||||
|
||||
1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following bash commands in parallel, each using the Bash tool:
|
||||
@@ -42,9 +44,8 @@ If and only if the user asks you to create a new git commit, follow these steps
|
||||
- Run a git diff command to see both staged and unstaged changes that will be committed.
|
||||
- Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
|
||||
|
||||
2. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in <commit_analysis> tags:
|
||||
2. Analyze all staged changes (both previously staged and newly added) and draft a commit message. When analyzing:
|
||||
|
||||
<commit_analysis>
|
||||
- List the files that have been changed or added
|
||||
- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
|
||||
- Brainstorm the purpose or motivation behind these changes
|
||||
@@ -55,7 +56,6 @@ If and only if the user asks you to create a new git commit, follow these steps
|
||||
- Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
|
||||
- Ensure the message is not generic (avoid words like "Update" or "Fix" without context)
|
||||
- Review the draft message to ensure it accurately reflects the changes and their purpose
|
||||
</commit_analysis>
|
||||
|
||||
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel:
|
||||
- Add relevant untracked files to the staging area.
|
||||
|
||||
@@ -37,7 +37,7 @@ export const BatchTool = Tool.define("batch", async () => {
|
||||
const discardedCalls = params.tool_calls.slice(10)
|
||||
|
||||
const { ToolRegistry } = await import("./registry")
|
||||
const availableTools = await ToolRegistry.tools("", "")
|
||||
const availableTools = await ToolRegistry.tools("")
|
||||
const toolMap = new Map(availableTools.map((t) => [t.id, t]))
|
||||
|
||||
const executeCall = async (call: (typeof toolCalls)[0]) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user