From 711702029b5e8f38ca696601daea4d7064ccec9b Mon Sep 17 00:00:00 2001 From: cskwork Date: Sun, 25 Oct 2026 21:45:19 +0900 Subject: [PATCH 1/6] feat(llm): add guarded OpenCode SDK bridge --- .github/workflows/ci.yml | 6 + README.es.md | 2 + README.ja.md | 2 + README.ko.md | 9 + README.md | 9 + README.zh-Hans.md | 2 + README.zh-Hant.md | 2 + adapters/opencode-sdk/.gitignore | 16 + adapters/opencode-sdk/README.es.md | 122 +++ adapters/opencode-sdk/README.ja.md | 122 +++ adapters/opencode-sdk/README.ko.md | 122 +++ adapters/opencode-sdk/README.md | 124 +++ adapters/opencode-sdk/README.zh-Hans.md | 122 +++ adapters/opencode-sdk/README.zh-Hant.md | 122 +++ adapters/opencode-sdk/bun.lock | 990 ++++++++++++++++++++ adapters/opencode-sdk/package.json | 28 + adapters/opencode-sdk/src/bridge.mjs | 137 +++ adapters/opencode-sdk/src/legacy-plugin.mjs | 79 ++ adapters/opencode-sdk/src/legacy.mjs | 365 ++++++++ adapters/opencode-sdk/src/native-cli.mjs | 131 +++ adapters/opencode-sdk/src/native-plugin.mjs | 85 ++ adapters/opencode-sdk/src/native.mjs | 168 ++++ adapters/opencode-sdk/src/protocol.mjs | 160 ++++ adapters/opencode-sdk/src/server.mjs | 15 + adapters/opencode-sdk/src/worker.mjs | 22 + adapters/opencode-sdk/test/legacy.test.mjs | 350 +++++++ adapters/opencode-sdk/test/native.test.mjs | 234 +++++ agent/provider.go | 58 +- agent/provider_sdk_test.go | 145 +++ db/config.go | 33 +- db/config_sdk_test.go | 94 ++ db/schema.sql | 4 +- docs/es/llm-providers.md | 53 +- docs/ja/llm-providers.md | 53 +- docs/ko/llm-providers.md | 57 +- docs/llm-providers.md | 57 +- docs/zh-Hans/llm-providers.md | 53 +- docs/zh-Hant/llm-providers.md | 53 +- locale/es.json | 58 +- locale/ja.json | 58 +- locale/opencode_sdk.go | 62 ++ locale/zh-Hans.json | 58 +- locale/zh-Hant.json | 58 +- opencodesdk/provider.go | 596 ++++++++++++ opencodesdk/provider_test.go | 429 +++++++++ server/locale_test.go | 4 +- server/opencode_sdk.go | 88 ++ server/opencode_sdk_test.go | 374 ++++++++ server/server.go | 17 +- server/server_mgmt.go | 32 +- server/task_llm.go | 2 +- web/src/app/(main)/system/llm/page.tsx | 67 +- web/src/i18n/i18n.test.mjs | 15 +- web/src/i18n/messages/es.ts | 14 +- web/src/i18n/messages/ja.ts | 15 +- web/src/i18n/messages/providers.ts | 28 +- web/src/i18n/messages/zh-hans.ts | 15 +- web/src/i18n/messages/zh-hant.ts | 15 +- web/src/lib/llm-profile-presets.test.mjs | 33 +- web/src/lib/llm-profile-presets.ts | 61 +- web/src/lib/types.ts | 2 +- 61 files changed, 6245 insertions(+), 62 deletions(-) create mode 100644 adapters/opencode-sdk/.gitignore create mode 100644 adapters/opencode-sdk/README.es.md create mode 100644 adapters/opencode-sdk/README.ja.md create mode 100644 adapters/opencode-sdk/README.ko.md create mode 100644 adapters/opencode-sdk/README.md create mode 100644 adapters/opencode-sdk/README.zh-Hans.md create mode 100644 adapters/opencode-sdk/README.zh-Hant.md create mode 100644 adapters/opencode-sdk/bun.lock create mode 100644 adapters/opencode-sdk/package.json create mode 100644 adapters/opencode-sdk/src/bridge.mjs create mode 100644 adapters/opencode-sdk/src/legacy-plugin.mjs create mode 100644 adapters/opencode-sdk/src/legacy.mjs create mode 100644 adapters/opencode-sdk/src/native-cli.mjs create mode 100644 adapters/opencode-sdk/src/native-plugin.mjs create mode 100644 adapters/opencode-sdk/src/native.mjs create mode 100644 adapters/opencode-sdk/src/protocol.mjs create mode 100644 adapters/opencode-sdk/src/server.mjs create mode 100644 adapters/opencode-sdk/src/worker.mjs create mode 100644 adapters/opencode-sdk/test/legacy.test.mjs create mode 100644 adapters/opencode-sdk/test/native.test.mjs create mode 100644 agent/provider_sdk_test.go create mode 100644 db/config_sdk_test.go create mode 100644 locale/opencode_sdk.go create mode 100644 opencodesdk/provider.go create mode 100644 opencodesdk/provider_test.go create mode 100644 server/opencode_sdk.go create mode 100644 server/opencode_sdk_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 921d1186..f759c642 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,12 @@ jobs: working-directory: adapters/agent - run: npm test && npm run check working-directory: adapters/agent + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.14" + - name: Verify OpenCode SDK adapter + working-directory: adapters/opencode-sdk + run: bun install --frozen-lockfile --ignore-scripts && bun run check && bun test --timeout 30000 - name: Test adapter against real backend env: SCOPEWEAVER_ADAPTER_E2E: "1" diff --git a/README.es.md b/README.es.md index 4b60812a..e7397bf6 100644 --- a/README.es.md +++ b/README.es.md @@ -126,6 +126,8 @@ activa ni contacta al proveedor. Coding Plan está restringido a herramientas of ScopeWeaver no figura entre ellas. Consulta [configuración de proveedores y límites de compatibilidad](docs/es/llm-providers.md). Para reducir tokens de nube, consulta [Controlar el uso de tokens](docs/es/llm-providers.md#controlar-el-uso-de-tokens). +El **puente OpenCode SDK** opcional admite `native` CLI/cliente **2.1.4** (predeterminado) y `legacy` SDK/CLI **1.19.4**, con token independiente y `providerID/modelID`. Norma de ScopeWeaver conserva herramientas y aprobaciones. El modo heredado mantiene JSON seguro, permite normalizar espacios/escapes, no conserva bytes originales y rechaza enteros inseguros. Ambos modos carecen de soporte Windows; las pruebas previas cubrieron macOS arm64 y el resultado CI alojado de Linux sigue pendiente. El modo LLM solo local lo rechaza porque el proveedor puede ser nube. Consulta [inicio, límites y preparación de degradación SDK](docs/es/llm-providers.md#opencode-sdk-bridge): los binarios anteriores no arrancan mientras existan perfiles SDK. + Los perfiles opcionales de **Ollama nativo** funcionan sin clave API. Tres plantillas cubren Ollama en el mismo equipo, en el host desde Docker y el servicio opcional `docker compose --profile local-llm`. Elegir una consulta los modelos instalados y rellena uno, dando preferencia a la compatibilidad con herramientas declarada; no descarga ni ejecuta modelos. Envían un contexto de 32K (`num_ctx` 32,768), una solicitud a la vez y keep-alive de 30 minutos. El ajuste opcional diff --git a/README.ja.md b/README.ja.md index fa568df3..a8047372 100644 --- a/README.ja.md +++ b/README.ja.md @@ -103,6 +103,8 @@ PostgreSQL のデータベース 1 つにつき、実行中の ScopeWeaver サ **LLM → 新規**フォームには、Z.ai General API 用の GLM-5.3 設定テンプレートと、明確に区別した Coding Plan の参考設定があります。自分の API キーを追加してください。テンプレートの選択では、保存、有効化、プロバイダーへの通信は行いません。Coding Plan は公式対応ツールのみに制限され、ScopeWeaver は一覧にありません。[プロバイダーの設定と対応範囲](docs/ja/llm-providers.md)を参照してください。クラウドのトークン使用量を抑える方法は[トークン使用量の管理](docs/ja/llm-providers.md#controlling-token-use)にあります。 +任意の **OpenCode SDK ブリッジ**は固定 `native` CLI/クライアント **2.1.4**(既定)と `legacy` SDK/CLI **1.19.4** に対応し、独立したブリッジトークンと `providerID/modelID` を使います。ホストツールと承認は ScopeWeaver の Norma が管理します。旧版は安全な JSON 構造を保持しますが、空白/エスケープを正規化でき、生バイトを保証せず、危険な整数を拒否します。両方とも Windows 非対応。過去の実行テストは macOS arm64、Linux のホステッド CI 結果は未確認です。下流がクラウドの場合があるためローカル専用 LLM では拒否します。[SDK 設定・制限・ダウングレード準備](docs/ja/llm-providers.md#opencode-sdk-bridge)を参照してください。SDK プロファイルが残ると旧バイナリは起動できません。 + 任意の**ネイティブ Ollama** プロファイルは、API キーなしで動作します。同じマシン、Docker からホスト、任意の `docker compose --profile local-llm` サービスの 3 テンプレートがあります。選択時はインストール済みモデルを読み、ツール対応を宣言したものを優先して設定しますが、ダウンロードや実行はしません。32K のコンテキスト(`num_ctx` 32,768)、同時リクエスト 1 件、キープアライブ 30 分を送ります。任意の**LLM のローカル限定ネットワーク**(既定で無効)は、ループバックと明示的に信頼した推論用 CIDR のみを許可し、LAN/Docker のアドレスにも許可リスト登録が必要です。すべてのエージェントのウェブ検索と更新の確認・ダウンロードもブロックし、ローカルのバージョン表示とロールバックは利用できます。対象用ツールのネットワーク接続は維持します。ホスト・リモートの Ollama では、別途 `OLLAMA_NO_CLOUD=1` が必要です。厳密モード以外では、クラウド形式、独自 URL、プロキシは引き続き動作します。ネイティブ要求は、待機中の判定・メインエージェント・補足の質問を次の空き枠に優先し、ネイティブ判定は 90 秒・ask の既定値と、上限付きのコンテキスト保護を持ちます。[ネイティブ Ollama](docs/ja/llm-providers.md#native-ollama-local-inference)を参照してください。 > **PostgreSQL** が必要です。探索には **LLM** の設定が必要です diff --git a/README.ko.md b/README.ko.md index e2bbcf75..2b7def47 100644 --- a/README.ko.md +++ b/README.ko.md @@ -118,6 +118,15 @@ PostgreSQL 데이터베이스 하나에는 ScopeWeaver 서버 프로세스를 [제공자 설정과 지원 범위](docs/ko/llm-providers.md)를 확인하세요. 클라우드 토큰 사용량은 [토큰 사용량 줄이기](docs/ko/llm-providers.md#토큰-사용량-줄이기)를 참고하세요. +선택형 **OpenCode SDK 브리지** 프로필은 고정된 `native` CLI·클라이언트 **2.1.4**(기본값)와 +`legacy` SDK·CLI **1.19.4**, 별도 브리지 토큰, `providerID/modelID`를 지원합니다. +호스트 도구와 승인은 ScopeWeaver의 Norma 런타임이 관리합니다. 레거시는 안전한 JSON 구조를 보존하며 +무해한 공백·이스케이프는 정규화될 수 있습니다. 원본 바이트는 보장하지 않으며 안전하지 않은 정수는 거부합니다. +두 런타임 모두 Windows는 지원하지 않습니다. 이전 실제 런타임 테스트 범위는 macOS arm64이며 +Linux의 호스팅된 CI 결과는 아직 없습니다. 하위 제공자가 클라우드일 수 있어 로컬 전용 LLM 모드에서는 거부됩니다. +[SDK 설정·제약·다운그레이드 준비](docs/ko/llm-providers.md#opencode-sdk-브리지)를 참고하세요. +SDK 프로필이 남아 있으면 이전 바이너리는 시작할 수 없습니다. + 선택 사항인 **기본 Ollama** 프로필은 API 키 없이 동작합니다. 같은 컴퓨터의 Ollama, Docker에서 접근하는 호스트 Ollama, 선택형 `docker compose --profile local-llm` 서비스용 템플릿 세 개를 제공합니다. 선택하면 서버에 설치된 모델을 조회하고 도구 호출 지원을 선언한 모델을 우선 채웁니다. diff --git a/README.md b/README.md index 01f086b5..e18430ee 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,15 @@ activate, or contact a provider. Coding Plan use is restricted to officially sup ScopeWeaver is not listed. See [provider setup and support limits](docs/llm-providers.md). To reduce cloud token use, see [Controlling token use](docs/llm-providers.md#controlling-token-use). +Optional **OpenCode SDK bridge** profiles support pinned `native` CLI/client **2.1.4** (default) and +`legacy` SDK/CLI **1.19.4**, with a separate bridge token and `providerID/modelID`. ScopeWeaver's +Norma runtime keeps host tools and approvals. Legacy preserves safe JSON structure, allowing benign +whitespace/escape normalization; it does not preserve original bytes and rejects unsafe integers. Both runtimes +are unsupported on Windows. Prior runtime tests cover macOS arm64; Linux hosted CI results are pending. +Local-only LLM mode rejects this bridge because its downstream provider may be cloud-based. See +[SDK setup, limits and downgrade preparation](docs/llm-providers.md#opencode-sdk-bridge); +older binaries cannot start with SDK profiles present. + Optional **native Ollama** profiles run without an API key. Three templates cover Ollama on the same machine, on the host from Docker, and the opt-in `docker compose --profile local-llm` service. Selecting one reads installed models and prefills one, preferring declared tool support; it does not download or run a model. They send a 32K context (`num_ctx` 32,768), one request at a time and a 30-minute keep-alive. An opt-in diff --git a/README.zh-Hans.md b/README.zh-Hans.md index e2640025..15581a4d 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -96,6 +96,8 @@ 「LLM → 新增」表单提供 Z.ai General API 的 GLM-5.3 配置模板,以及另行标示的 Coding Plan 参考配置。请自行加入 API 密钥;选取模板不会保存、启用或联络供应商。Coding Plan 仅限官方支持的工具使用,ScopeWeaver 不在清单内。详见[供应商配置与支持限制](docs/zh-Hans/llm-providers.md)。若要减少云端 token 用量,请参阅[控制 token 用量](docs/zh-Hans/llm-providers.md#controlling-token-use)。 +可选的 **OpenCode SDK 桥接器**支持固定的 `native` CLI/客户端 **2.1.4**(默认)与 `legacy` SDK/CLI **1.19.4**,使用独立桥接令牌及 `providerID/modelID`。宿主工具和审批仍由 ScopeWeaver 的 Norma 管理。旧版保留安全 JSON 结构,但可规范化空白/转义,不保留原始字节,并拒绝不安全整数。两个运行时均不支持 Windows;先前运行测试范围为 macOS arm64,Linux 托管 CI 结果尚待验证。下游可能是云端,因此仅本地 LLM 模式拒绝桥接器。见 [SDK 设置、限制与降级准备](docs/zh-Hans/llm-providers.md#opencode-sdk-bridge);SDK 配置仍存在时,旧二进制无法启动。 + 可选的**原生 Ollama** 配置不需要 API 密钥。三种模板分别适用于同一台电脑、从 Docker 存取主机,以及选择性启用的 `docker compose --profile local-llm` 服务。选取模板会读取已安装模型并预填一个模型,优先选择声明支持工具的模型;不会下载或运行模型。默认传送 32K 上下文(`num_ctx` 32,768)、每次一个请求,以及 30 分钟的 keep-alive。选择性启用的**仅限本地 LLM 网络**配置默认关闭,仅允许环回地址与明确信任的推论 CIDR;LAN 与 Docker 地址必须加入允许清单。此配置也会封锁所有智能体网页搜寻与更新检查/下载,但仍可查看本地版本与回滚。目标工具仍可存取网络;主机/远程 Ollama 必须另设 `OLLAMA_NO_CLOUD=1`。非严格模式仍可使用云端格式、自定义 URL 与代理服务器。原生请求会将下一个可用名额优先提供给排队中的裁决/主要智能体/侧问请求;原生裁决默认为 90 秒/ask,并有上下文边界保护。详见[原生 Ollama](docs/zh-Hans/llm-providers.md#native-ollama-local-inference)。 > 需要 **PostgreSQL** 数据库。探索需要已配置的 **LLM**(`ANTHROPIC_API_KEY`、`OPENAI_API_KEY`、免密钥 Ollama 配置,或在界面中配置)。 diff --git a/README.zh-Hant.md b/README.zh-Hant.md index 0df9ac54..22c3d11f 100644 --- a/README.zh-Hant.md +++ b/README.zh-Hant.md @@ -96,6 +96,8 @@ 「LLM → 新增」表單提供 Z.ai General API 的 GLM-5.3 設定範本,以及另行標示的 Coding Plan 參考設定。請自行加入 API 金鑰;選取範本不會儲存、啟用或聯絡供應商。Coding Plan 僅限官方支援的工具使用,ScopeWeaver 不在清單內。詳見[供應商設定與支援限制](docs/zh-Hant/llm-providers.md)。若要減少雲端 token 用量,請參閱[控制 token 用量](docs/zh-Hant/llm-providers.md#controlling-token-use)。 +選用的 **OpenCode SDK 橋接器**支援固定的 `native` CLI/用戶端 **2.1.4**(預設)與 `legacy` SDK/CLI **1.19.4**,使用獨立橋接權杖及 `providerID/modelID`。主機工具與核准仍由 ScopeWeaver 的 Norma 管理。舊版保留安全 JSON 結構,但可正規化空白/跳脫,不保留原始位元組,並拒絕不安全整數。兩個執行環境皆不支援 Windows;先前測試範圍為 macOS arm64,Linux 託管 CI 結果尚待驗證。下游可能是雲端,因此僅本機 LLM 模式拒絕橋接器。請見 [SDK 設定、限制與降級準備](docs/zh-Hant/llm-providers.md#opencode-sdk-bridge);SDK 設定檔仍存在時,舊二進位檔無法啟動。 + 可選的**原生 Ollama** 設定檔不需要 API 金鑰。三種範本分別適用於同一台電腦、從 Docker 存取主機,以及選擇性啟用的 `docker compose --profile local-llm` 服務。選取範本會讀取已安裝模型並預填一個模型,優先選擇宣告支援工具的模型;不會下載或執行模型。預設傳送 32K 上下文(`num_ctx` 32,768)、每次一個請求,以及 30 分鐘的 keep-alive。選擇性啟用的**僅限本機 LLM 網路**設定預設關閉,僅允許回送位址與明確信任的推論 CIDR;LAN 與 Docker 位址必須加入允許清單。此設定也會封鎖所有代理程式網頁搜尋與更新檢查/下載,但仍可查看本機版本與復原。目標工具仍可存取網路;主機/遠端 Ollama 必須另設 `OLLAMA_NO_CLOUD=1`。非嚴格模式仍可使用雲端格式、自訂 URL 與代理伺服器。原生請求會將下一個可用名額優先提供給排隊中的裁決/主要代理程式/側問請求;原生裁決預設為 90 秒/ask,並有上下文邊界保護。詳見[原生 Ollama](docs/zh-Hant/llm-providers.md#native-ollama-local-inference)。 > 需要 **PostgreSQL** 資料庫。探索需要已設定的 **LLM**(`ANTHROPIC_API_KEY`、`OPENAI_API_KEY`、免金鑰 Ollama 設定檔,或在介面中設定)。 diff --git a/adapters/opencode-sdk/.gitignore b/adapters/opencode-sdk/.gitignore new file mode 100644 index 00000000..1905390a --- /dev/null +++ b/adapters/opencode-sdk/.gitignore @@ -0,0 +1,16 @@ +node_modules/ +.env +.env.* +provider-config*.json +credentials*.json +auth.json +*.secret.json +*.secrets.json +secrets/ +credentials/ +*.local.json +*.log +.runtime/ +.cache/ +tmp/ +coverage/ diff --git a/adapters/opencode-sdk/README.es.md b/adapters/opencode-sdk/README.es.md new file mode 100644 index 00000000..d7a4aa19 --- /dev/null +++ b/adapters/opencode-sdk/README.es.md @@ -0,0 +1,122 @@ +# Puente OpenCode SDK + +[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md) + +Puente opcional limitado a loopback para **CLI/cliente/SDK nativo de OpenCode 2.1.4** y la pareja **SDK/CLI heredada 1.19.4**, probada por separado. ScopeWeaver conserva la conversación, ejecución de herramientas, aprobaciones, controles de alcance, reintentos y compactación. El puente transmite un solo paso del modelo y captura solicitudes de herramientas; no ejecuta herramientas de ScopeWeaver ni herramientas internas de OpenCode. Los proveedores API y OpenCode Go existentes funcionan sin este puente. + +## Inicio + +El entorno de CI probado es **Bun 1.3.14**. No se han verificado versiones posteriores. + +```sh +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +El token del puente debe contener al menos **32 caracteres no blancos**. El servidor CLI nativo privado usa una contraseña independiente generada por llamada (`OPENCODE_PASSWORD`) y autenticación HTTP Basic. No es el token del puente ni una clave del proveedor. + +En el perfil de proveedor de ScopeWeaver, elige `opencode-sdk`, configura `http://127.0.0.1:4097` e introduce ese **token del puente**. Es distinto de una clave API de nube. No reutilices un token de OpenCode Go/Console. Carga los modelos y selecciona el `providerID/modelID` exacto. + +El entorno predeterminado, sin iniciar sesión, usa un catálogo aislado y el descubrimiento público. Solo realiza solicitudes GET al catálogo: **nunca inferencia**. Muestra únicamente modelos OpenCode de precio cero presentes en la lista pública y reconocidos por la versión fijada. Los fallos del catálogo producen un error explícito. Figurar en la lista no acredita elegibilidad, cuota disponible ni éxito de inferencia. Los modelos gratuitos pueden cambiar o desaparecer; no hay sustitución predeterminada por modelos de pago ni por otras credenciales. + +**Verificación anterior del 25 de octubre de 2026 (KST):** `opencode/deepseek-v4.1-flash-free` completó una conversación real de ScopeWeaver mediante el puente de producción CLI/cliente nativo 2.1.4. Tres inferencias reales cubrieron la prueba de conexión de la interfaz, una solicitud de herramienta del anfitrión revisada y aprobada antes de afectar al archivo y la respuesta final visible `FREE_OK`. El modelo procedía del catálogo predeterminado de precio cero. No se usaron claves API, importaciones de cuentas, simulaciones de modelos/respuestas, identidades falsificadas ni modelos de pago alternativos. Las herramientas nativas siguieron bloqueadas y ScopeWeaver ejecutó la acción del anfitrión. Es evidencia previa de esa fecha, no una repetición realizada ahora. + +La inferencia nativa usa el CLI auténtico fijado y las API oficiales del cliente; el descubrimiento sin inferencia usa el SDK integrado. La ruta anterior del SDK integrado dedicada a capturar herramientas recibió HTTP 403 `FreeTierError`. El acceso gratuito alojado de la versión heredada sigue sin verificarse. Los modelos y la elegibilidad pueden cambiar; la prueba fechada no establece una cuota numérica ni disponibilidad futura. Un rechazo estructurado se devuelve como `free_tier_restricted`, sin elegir un modelo de pago. + +`SCOPEWEAVER_OPENCODE_PORT` modifica el puerto predeterminado 4097. `SCOPEWEAVER_OPENCODE_HOST` debe ser `127.0.0.1`. Se rechazan solicitudes de navegador con `Origin` y URL que no sean de loopback. Todas las rutas requieren el token: protégelo como una contraseña. + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` limita la llamada completa entre **0 y 86,400,000 ms**. El valor predeterminado **0** deja la duración bajo el control de cancelación del anfitrión; no hay un límite oculto de dos minutos. La cancelación y el cierre tienen un plazo independiente de **1.5 segundos** para terminar por la fuerza. La consulta del catálogo público tiene **10 segundos**. Los límites son **16 MiB** por solicitud, **2,000,000 caracteres** por JSON de argumentos, **8,000,000 caracteres** para la suma de texto/razonamiento/argumentos del modelo nativo y **20,000,000 bytes** de salida del trabajador. Superarlos causa un error explícito, sin truncar datos. + +## Configuración dedicada del proveedor + +Para un modelo explícito compatible con OpenAI o una prueba local, indica en `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` un archivo JSON administrado por el operador del puente. Guárdalo fuera del checkout o con el nombre ignorado por Git `provider-config.local.json`: + +```json +{ + "providers": { + "local": { + "name": "Local model", + "package": "@opencode/ai/providers/openai-compatible", + "settings": { + "baseURL": "http://127.0.0.1:8000/v1", + "apiKey": "explicit-provider-key-if-needed" + }, + "models": { + "my-model": { + "name": "My model", + "capabilities": { "tools": true }, + "limit": { "context": 200000, "output": 32000 }, + "compatibility": { "maxTokensField": "max_tokens" } + } + } + } + } +} +``` + +Selecciona `local/my-model`. El archivo sustituye el descubrimiento predeterminado de ese puente. No carga configuración OpenCode de usuario/proyecto, archivos `.env`, complementos, servidores MCP ni credenciales guardadas. Solo entrega al proveedor su `apiKey` explícita. Un destino remoto puede cobrar la inferencia; configurarlo no lo convierte en gratuito. Se rechazan paquetes SDK arbitrarios y claves de configuración adicionales. + +## Límites de ejecución + +Cada llamada usa un trabajador Bun separado, un entorno limitado a una lista permitida y rutas temporales propias de home/configuración/datos. El modo nativo controla un servidor privado del CLI oficial, una ejecución real `run`, su base de datos temporal y un transporte de salida loopback autenticado de un solo uso. El cliente oficial gestiona descubrimiento, eventos de sesión e interrupción. No se heredan credenciales de nube, archivos de autenticación del usuario, proxies ni precargas. Se usan directamente los binarios opcionales de plataforma fijados; `--ignore-scripts` sigue siendo válido y no hace falta instalación global. **No omitas las dependencias opcionales**. + +Se conservan las definiciones y las implementaciones originales de las herramientas nativas de OpenCode. Todos sus permisos son ASK y no se usa `--auto`. Un hook público `execute.before` rechaza de manera independiente, antes de ejecutarse, cualquier herramienta ajena al anfitrión, incluida la generada `execute`. Una solicitud nativa hace fallar la llamada del puente y no llega a ScopeWeaver. Solo las funciones registradas de captura del anfitrión pueden esperar y convertirse en solicitudes de herramientas del anfitrión; se eliminan los demás agentes. Los alias sin colisiones `sw_tool_N` evitan reemplazar herramientas nativas. Las descripciones conservan el nombre y texto originales, y las respuestas restauran el nombre del anfitrión. + +Los segmentos system del anfitrión se unen con dos saltos de línea y se pasan sin cambios mediante la configuración pública del agente personalizado. También se conserva el contexto generado por OpenCode sobre modelo, entorno y herramientas. La política final informa de que solo se permiten funciones marcadas `Host tool:`. El hook público context mantiene roles, ID/resultados de herramientas y límites/configuración por llamada. No se garantiza que system contenga solo los segmentos del anfitrión. Se transmiten campos thinking/effort específicos del modelo; las opciones no admitidas fallan explícitamente. Un límite de salida declarado menor reduce el máximo solicitado. Entrada, salida y caché se contabilizan por separado. + +Se bloquea una segunda solicitud al modelo. El transporte privado reenvía la solicitud auténtica al URL original, rechaza redirecciones y no sustituye destino, modelo, identidad ni credenciales. Las herramientas capturadas solo se entregan después de interrumpir, borrar la sesión y limpiar los procesos propios. Desconexión, DELETE, vencimiento y cierre terminan el grupo POSIX del trabajador y lo fuerzan después de **1.5 segundos**. Esa limpieza del árbol de procesos no está implementada en Windows: **ambos modos, nativo y heredado, rechazan inferencia en Windows**. + +La configuración pública del CLI no permite desactivar por completo la búsqueda de configuración de sistema/MDM. El modo nativo rechaza el inicio con `configuration_error` si existe `/Library/Application Support/opencode` o `ai.opencode.managed.plist` bajo `/Library/Managed Preferences` global/del usuario en macOS, o `/etc/opencode` en Linux. También rechaza rutas inaccesibles y enlaces simbólicos rotos. No lee ni modifica esas fuentes. + +El modo nativo conserva el JSON original de argumentos, incluidos enteros de 64 bits, en respuestas completas, flujos e historial de herramientas del anfitrión. La proyección analizada por JavaScript nunca se ejecuta. Rechaza números inseguros fuera de los argumentos originales autoritativos, contenido no admitido, herramientas nativas desconocidas, ID duplicados y respuestas malformadas. Los esquemas admiten definiciones en línea, JSON Pointer local `$ref`/`$defs` y uniones; rechazan referencias remotas, `$id`, `$dynamicRef` y referencias sin resolver. La inicialización aún carga código propio de proveedores/catálogo: es una frontera de la aplicación, **no un aislamiento del sistema operativo**. Un puente loopback puede usar un modelo remoto y no cumple la política de inferencia solo local de ScopeWeaver. + +## Versiones y límites del modo heredado + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | Pareja fijada | Capacidad `runtime` | +| --- | --- | --- | +| `native` (predeterminado) | CLI oficial, `@opencode/client` y SDK **2.1.4** | `native` | +| `legacy` | `@opencode-ai/sdk/v2` y CLI **1.19.4** | `legacy` | + +Define `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` antes de iniciar para usar la API antigua. La verificación local previa de ambos entornos reales cubrió **macOS arm64**. CI está configurada para pruebas reales en Linux, pero su resultado alojado sigue pendiente. Otras versiones SDK se rechazan antes de inferir; no se garantiza compatibilidad universal entre versiones y plataformas. + +El modo heredado usa directamente el binario oficial opcional correspondiente. Por eso admite `--ignore-scripts`: no llama a un envoltorio sin inicializar ni a una instalación global de `opencode`. No omitas las dependencias opcionales. Su CLI y el servidor MCP loopback de captura terminan con el grupo POSIX del trabajador. Windows no dispone de esa limpieza. Cada llamada usa una copia privada del paquete del complemento y de su archivo de bloqueo, sin enlaces escribibles a dependencias compartidas. + +El modo heredado admite JSON/historial estructuralmente válido y representable con seguridad. Puede normalizar espacios y escapes HTML/Unicode equivalentes; no conserva los bytes léxicos originales. Rechaza explícitamente enteros inseguros cuyo valor absoluto supere **9,007,199,254,740,991**, historial de razonamiento opaco/firmado y sobrescrituras de proveedor no admitidas. Usa el modo nativo para conservar argumentos originales de 64 bits. Los proveedores heredados personalizados usan la configuración explícita compatible con OpenAI anterior, pero solo se mapean la compatibilidad `max_tokens`, las capacidades de herramientas admitidas y los límites de contexto/salida. Se rechazan `reasoningField` y sobrescrituras de capacidades no admitidos. Los perfiles API/Go siguen disponibles para entradas fuera de los límites probados de ambos modos SDK. + +## Protocolo y comprobaciones + +Protocolo **`scopeweaver-opencode-sdk`, versión 1**, con rutas autenticadas: `GET /v1/capabilities`, `GET /v1/models`, `POST /v1/complete`, `POST /v1/stream` y `DELETE /v1/requests/{request_id}`. El flujo NDJSON transmite deltas reales de texto/razonamiento, solicitudes capturadas y uso final; en caso de éxito hay exactamente un `message_stop`. Los errores usan códigos acotados y mensajes con datos del proveedor ocultos. Solo un agotamiento estructurado explícito de facturación/cuota se clasifica como `quota_exhausted` (HTTP 402 al completar); límites genéricos de frecuencia, autenticación y 5xx no se reclasifican. Un exceso estructurado de contexto devuelve `context_length_exceeded` (HTTP 400), manteniendo la señal de compactación del anfitrión; no basta un 400 genérico ni palabras en el mensaje. `FreeTierError` estructurado produce `free_tier_restricted` (HTTP 403), separado de cuota y prohibiciones genéricas. El puente no elige un modelo alternativo; el anfitrión decide según su política configurada. + +Ejemplo de solicitud completa; el endpoint de flujo acepta el mismo cuerpo: + +```json +{ + "request_id": "unique-call-uuid", + "session_id": "stable-host-conversation-uuid", + "model": "local/my-model", + "system": ["Host system instructions"], + "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }], + "tools": [], + "max_tokens": 64, + "thinking": "disabled" +} +``` + +Campos opcionales: `session_id`, `temperature`, `stop` (lista de cadenas), `thinking`, `thinking_type` y `reasoning_effort`. Los bloques admitidos son `text`, `thinking`/`tool_use` del asistente y `tool_result` del usuario con texto. Las herramientas declaran `name`, `description` e `input_schema`. La respuesta completa contiene `message`, `stop_reason` (`end_turn`, `tool_use` o `max_tokens`) y `usage` con entrada/salida/lectura de caché/escritura de caché separadas. Consulta el contrato y límites en [protocol.mjs](src/protocol.mjs) y el transporte en [bridge.mjs](src/bridge.mjs). + +En `opencode-go`, `session_id` mantiene estable `x-opencode-session` entre sesiones SDK nuevas por llamada; si falta se usa `request_id`. `opencode` conserva las cabeceras genuinas de sesión e identidad generadas por el entorno. Los proveedores genéricos no reciben cabeceras de sesión específicas de la pasarela. Se conserva el User-Agent auténtico del CLI. + +```sh +bun run check +bun test --timeout 30000 +``` + +Las pruebas usan la pareja real fijada de CLI/cliente/SDK con modelos SSE locales. Verifican descubrimiento sin inferencia, historial/configuración, captura segura, flujo/uso, argumentos numéricos originales, llamadas nativas hostiles, aislamiento concurrente y cancelación. No prueban la elegibilidad real de modelos gratuitos alojados. + +## Perfiles de ScopeWeaver y límite de degradación + +Un token vacío conserva el guardado solo para el mismo endpoint SDK. Al cambiar URL o formato no se reutiliza; `localhost`, `127.0.0.1` y `::1` son ámbitos distintos de credenciales. La plantilla SDK borra claves de nube sin guardar, proxy y cabecera de sesión personalizada, deja el modelo vacío y no descubre, infiere, guarda ni activa. Usa **Cargar modelos** de forma explícita; **Probar conexión** solo hace inferencia al pulsarlo. + +La restricción `CHECK` de formatos de perfil añade `opencode-sdk`. Los binarios antiguos no arrancan mientras queden perfiles SDK. Antes de degradar, respalda la base de datos y usa la versión actual para convertirlos a un formato antiguo compatible o eliminarlos. La verificación previa de migración usó bases desechables, sin migrar una base de producción. Consulta la [guía de proveedores](../../docs/es/llm-providers.md#opencode-sdk-bridge). diff --git a/adapters/opencode-sdk/README.ja.md b/adapters/opencode-sdk/README.ja.md new file mode 100644 index 00000000..8f087d8c --- /dev/null +++ b/adapters/opencode-sdk/README.ja.md @@ -0,0 +1,122 @@ +# OpenCode SDK ブリッジ + +[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md) + +任意で使用するループバック専用ブリッジです。固定した **OpenCode ネイティブ CLI/クライアント/SDK 2.1.4** と、別途テストした **旧 SDK/CLI 1.19.4** の組み合わせに対応します。対話、ツール実行、承認、スコープ検査、再試行、圧縮は ScopeWeaver が管理します。ブリッジはモデルの 1 ステップをストリーム配信してツール要求を収集し、ScopeWeaver のツールや OpenCode の組み込みツールを実行しません。従来の API/OpenCode Go プロバイダーはブリッジなしで利用できます。 + +## 起動 + +テスト済み CI ランタイムは **Bun 1.3.14** です。新しいバージョンは未検証です。 + +```sh +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +ブリッジトークンは**空白以外の 32 文字以上**が必要です。非公開のネイティブ CLI サーバーは呼び出しごとに生成する独立したパスワード(`OPENCODE_PASSWORD`)と HTTP Basic 認証を使います。ブリッジトークンや上流プロバイダーキーとは別です。 + +ScopeWeaver のプロバイダープロファイルで `opencode-sdk`、URL `http://127.0.0.1:4097` を選び、上記の**ブリッジトークン**を入力します。クラウド API キーとは別です。OpenCode Go/Console のトークンを流用しないでください。モデル一覧を読み込んで正確な `providerID/modelID` を選びます。 + +既定の未ログインランタイムは、独立したカタログと公開モデル探索を使用します。探索は公開カタログへの GET のみで、**推論を実行しません**。公開一覧に現在存在し、固定ランタイムが認識する料金ゼロの OpenCode モデルだけを表示します。カタログ取得失敗はエラーです。掲載は利用資格、残り割当量、推論成功の証明ではありません。無料モデルは変更・削除される場合があり、既定では有料モデルや認証情報へのフォールバックを行いません。 + +**2026-10-25(KST)の過去の検証:** 製品のネイティブ CLI/クライアント 2.1.4 ブリッジを通じ、`opencode/deepseek-v4.1-flash-free` で実際の ScopeWeaver 対話が完了しました。実推論 3 回で UI 接続テスト、ファイルに影響する前に確認・承認したホストツール要求、最終画面の回答 `FREE_OK` を確認しました。既定の料金ゼロのカタログを使用し、API キー、アカウントの取り込み、モデル/応答のモック、識別情報の偽装、有料モデルへの切替は使用していません。ネイティブツールは阻止され、ホスト操作は ScopeWeaver が実行しました。これは当日の既存の証拠であり、今回の再実行結果ではありません。 + +ネイティブ推論は固定された本物の CLI と公式クライアント API を使用し、推論なしの探索には埋め込み SDK を使います。以前の収集専用の埋め込み SDK 経路は HTTP 403 `FreeTierError` で拒否されました。旧版でのホステッド無料アクセスは未検証です。モデルや利用資格は変わるため、当日のテストは数値的な割当量や継続提供を保証しません。構造化された拒否は `free_tier_restricted` として返し、有料モデルを選びません。 + +`SCOPEWEAVER_OPENCODE_PORT` で既定の 4097 を変更できます。`SCOPEWEAVER_OPENCODE_HOST` は `127.0.0.1` に固定します。ブラウザーの `Origin` 付き要求と非ループバック URL は拒否します。すべての API 経路にトークンが必要です。パスワード同様に保護してください。 + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` は呼び出し全体の期限 **0–86,400,000 ミリ秒**です。既定 **0** はホストのキャンセルに委ね、隠れた 2 分の推論期限はありません。キャンセル/終了には別途 **1.5 秒**の強制終了期限があります。公開カタログ取得は **10 秒**です。要求本文は **16 MiB**、ツール引数 JSON 1 件は **2,000,000 文字**、ネイティブモデルのテキスト/思考/引数合計は **8,000,000 文字**、ワーカー出力は **20,000,000 バイト**が上限です。超過時は明示的に失敗し、データを切り詰めません。 + +## 専用プロバイダー設定 + +明示的な OpenAI 互換モデルやローカルテストモデルには、運用者が管理する JSON ファイルを `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` に指定します。チェックアウト外、または Git が無視する `provider-config.local.json` という名前で保存します。 + +```json +{ + "providers": { + "local": { + "name": "Local model", + "package": "@opencode/ai/providers/openai-compatible", + "settings": { + "baseURL": "http://127.0.0.1:8000/v1", + "apiKey": "explicit-provider-key-if-needed" + }, + "models": { + "my-model": { + "name": "My model", + "capabilities": { "tools": true }, + "limit": { "context": 200000, "output": 32000 }, + "compatibility": { "maxTokensField": "max_tokens" } + } + } + } + } +} +``` + +`local/my-model` を選択します。このファイルはブリッジの既定探索を置き換えます。ユーザー/プロジェクトの OpenCode 設定、`.env`、プラグイン、MCP サーバー、保存済み認証情報を読み込みません。明示した `apiKey` のみを対象プロバイダーに渡します。リモート推論は有料の場合があり、設定を追加しても無料にはなりません。任意の SDK パッケージや余分な設定キーは拒否します。 + +## 実行境界 + +呼び出しごとに独立した Bun ワーカーを使い、環境変数を許可リストに限定し、一時 home/設定/データを所有します。ネイティブモードは非公開の公式 CLI サーバー、実際の CLI `run`、一時 DB、1 回限りの認証付きループバック出口転送を所有します。公式クライアントが探索、セッションイベント、中断を処理します。クラウド認証情報、ユーザー認証ファイル、プロキシ、プリロード設定を継承しません。固定されたオプション依存のプラットフォームバイナリを直接使うため、`--ignore-scripts` が有効でグローバルインストールは不要です。**オプション依存を省略しないでください**。 + +OpenCode 本来の組み込みツール定義と実装を保持します。ネイティブ権限はすべて ASK で、CLI `--auto` は使いません。公開 `execute.before` ポリシーフックが、生成された `execute` を含むすべての非ホストツールを実行前に独立して拒否します。ネイティブツール要求はブリッジ要求を失敗させ、ScopeWeaver に渡しません。登録されたホスト収集関数だけが待機し、ホストツール要求になります。他のエージェントは削除します。衝突しない `sw_tool_N` 別名で既定ツールの上書きを防ぎ、説明には正確な元の名前・説明を残し、応答では元のホスト名を復元します。 + +ホストの system 部分を 2 改行で結合し、公開カスタムエージェント system 設定にそのまま渡します。OpenCode が生成するモデル/環境/ツールの文脈も保持します。最後のポリシー部分は `Host tool:` と記された関数だけが有効だと伝えます。公開 context フックでホストの役割、ツール ID/結果、呼び出しごとの制限・設定を保持します。system 配列がホスト部分のみになる保証はありません。モデル別の thinking/effort フィールドを転送し、未対応設定は明示的に失敗します。モデルの出力上限が要求より小さい場合はその上限に抑えます。入力、出力、キャッシュ使用量を分けて扱います。 + +2 回目のモデル要求は阻止します。非公開出口は本物の CLI が生成した要求を元の上流 URL に転送し、リダイレクトを拒否します。宛先、モデル、識別情報、認証情報を差し替えません。ホストツール要求は中断、セッション削除、所有プロセスの後片付け後にのみ返します。切断、DELETE、タイムアウト、終了で POSIX ワーカープロセスグループを停止し、**1.5 秒**後に強制終了します。このプロセスツリーの後片付けは Windows に未実装のため、**ネイティブと旧版の両方で Windows の推論を拒否**します。 + +CLI の公開設定ではシステム/MDM 設定探索を完全に無効化できません。macOS の `/Library/Application Support/opencode`、ユーザー/全体の `/Library/Managed Preferences` 下の `ai.opencode.managed.plist`、Linux の `/etc/opencode` が存在すると、ネイティブモードは `configuration_error` で起動を拒否します。アクセス不能な場所や壊れたシンボリックリンクも拒否し、その設定を読み取り・変更しません。 + +ネイティブモードは完了/ストリーム応答とホスト履歴の再生で、64 ビット整数を含むツール引数の生 JSON を保持します。JavaScript で解析した値は実行しません。正本となる生引数以外の危険な数値、未対応コンテンツ、不明なネイティブツール、重複ツール ID、不正な応答は拒否します。スキーマはインライン定義、ローカル JSON Pointer `$ref`/`$defs`、ユニオンに対応し、リモート参照、`$id`、`$dynamicRef`、未解決参照を拒否します。初期化では本来のプロバイダー/カタログコードを読み込みます。これはアプリケーションの境界であり、**OS サンドボックスではありません**。ループバックの先にクラウドモデルがあり得るため、ScopeWeaver のローカル専用推論条件を満たしません。 + +## バージョンと旧版の制限 + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定した組み合わせ | 機能応答 `runtime` | +| --- | --- | --- | +| `native`(既定) | 公式 CLI、`@opencode/client`、SDK **2.1.4** | `native` | +| `legacy` | `@opencode-ai/sdk/v2`、CLI **1.19.4** | `legacy` | + +起動前に `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` を指定すると旧 API を選べます。過去に実際のランタイムをローカル検証した範囲は **macOS arm64** です。Linux CI に実ランタイムテストを設定していますが、ホステッド実行結果は未確認です。他の SDK バージョンは推論前に拒否します。任意のバージョン/プラットフォームの互換性は保証しません。 + +旧版は対応する公式オプションバイナリを直接使用するため、`--ignore-scripts` が有効です。未初期化のラッパーやグローバル `opencode` は使いません。オプション依存を省略しないでください。所有する CLI とループバック MCP 収集サーバーは、要求ワーカーの POSIX プロセスグループと共に終了します。Windows ではこの境界を実装していません。呼び出しごとに専用のプラグインパッケージとロックのコピーを使い、共有依存への書き込み可能なリンクを作りません。 + +旧版は構造的に正しく安全に表現できる JSON/履歴のみ対応します。空白や同値の HTML/Unicode エスケープは正規化されるため、元の字句・バイトを保証しません。絶対値が **9,007,199,254,740,991** を超える危険な整数、不透明/署名付き思考履歴、未対応のプロバイダー上書きは明示的に失敗します。生の 64 ビット引数が必要ならネイティブを使います。旧版のカスタムプロバイダーは上の明示的 OpenAI 互換設定を使いますが、`max_tokens` 互換フィールド、対応ツール機能、コンテキスト/出力上限のみをマッピングします。未対応の `reasoningField` と機能上書きは拒否します。SDK の境界外の入力には既存 API/Go プロファイルを使えます。 + +## プロトコルと検証 + +プロトコルは **`scopeweaver-opencode-sdk`、バージョン 1**。認証経路は `GET /v1/capabilities`、`GET /v1/models`、`POST /v1/complete`、`POST /v1/stream`、`DELETE /v1/requests/{request_id}` です。ストリームは NDJSON で、実際のネイティブテキスト/思考差分、収集したツール要求、最終使用量を送り、成功時は `message_stop` を 1 回だけ返します。エラーは限定した機械コードと機密を除いたメッセージを使用します。明示的で構造化された請求/割当量枯渇だけを `quota_exhausted`(完了要求 HTTP 402)とし、一般のレート制限、認証、5xx を枯渇へ置き換えません。構造化されたコンテキスト超過は `context_length_exceeded`(HTTP 400)としてホストの圧縮信号を維持します。一般の 400 やメッセージ内の語句だけでは分類しません。構造化された `FreeTierError` は `free_tier_restricted`(HTTP 403)で、枯渇や通常の禁止とは分けます。ブリッジは代替モデルを選ばず、ホストの設定済みポリシーが次の処理を決めます。 + +完了要求例(ストリーム経路も同じ本文を受け付けます): + +```json +{ + "request_id": "unique-call-uuid", + "session_id": "stable-host-conversation-uuid", + "model": "local/my-model", + "system": ["Host system instructions"], + "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }], + "tools": [], + "max_tokens": 64, + "thinking": "disabled" +} +``` + +任意フィールドは `session_id`、`temperature`、文字列配列 `stop`、`thinking`、`thinking_type`、`reasoning_effort`。ホストコンテンツは `text`、assistant の `thinking`/`tool_use`、テキスト結果を持つ user の `tool_result` に対応します。ツール定義は `name`、`description`、`input_schema` を使います。完了応答は `message`、`stop_reason`(`end_turn`、`tool_use`、`max_tokens`)と入力/出力/キャッシュ読み取り/書き込みを分けた `usage` を含みます。公開契約と制限は [protocol.mjs](src/protocol.mjs)、転送は [bridge.mjs](src/bridge.mjs) を参照してください。 + +`opencode-go` では `session_id` により、呼び出しごとの新 SDK セッションでも `x-opencode-session` を一定にします。未指定なら `request_id` を使います。`opencode` は実ランタイム生成のセッション/識別ヘッダーを維持します。通常のプロバイダーにゲートウェイ専用ヘッダーは付けません。本物の CLI の User-Agent を保持します。 + +```sh +bun run check +bun test --timeout 30000 +``` + +固定した実際の CLI/クライアント/SDK とローカル SSE モデルで、推論なし探索、ホスト履歴/設定、収集の安全性、ストリーム/使用量、生の数値引数、悪意あるネイティブ要求、同時実行の隔離、キャンセルを検証します。ホステッド無料モデルの実際の利用資格は証明しません。 + +## ScopeWeaver プロファイルとダウングレード境界 + +空のトークンで保存済み値を保持できるのは同じ SDK エンドポイントだけです。宛先や形式を変えた際に古いトークンを流用しません。`localhost`、`127.0.0.1`、`::1` は別の認証スコープです。SDK テンプレートは未保存のクラウドキー、プロキシ、独自セッションヘッダーを消し、モデルを空にします。探索・推論・保存・有効化は行いません。**モデル読み込み**を明示的に実行し、**接続テスト**はクリック時だけ推論します。 + +プロファイル DB の `CHECK` 制約に `opencode-sdk` を追加します。SDK プロファイルが残ると古いバイナリは起動できません。ダウングレード前に DB をバックアップし、現在のバージョンで旧対応形式へ変換するか SDK プロファイルを削除します。過去の移行検証は使い捨て DB のみで、稼働中の DB は移行していません。[プロバイダーガイド](../../docs/ja/llm-providers.md#opencode-sdk-bridge)を参照してください。 diff --git a/adapters/opencode-sdk/README.ko.md b/adapters/opencode-sdk/README.ko.md new file mode 100644 index 00000000..007995a5 --- /dev/null +++ b/adapters/opencode-sdk/README.ko.md @@ -0,0 +1,122 @@ +# OpenCode SDK 브리지 + +[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md) + +**네이티브 OpenCode CLI·클라이언트·SDK 2.1.4**와 따로 검증한 **구형 SDK·CLI 1.19.4**를 연결하는 선택 기능입니다. ScopeWeaver가 대화 기록, 도구 실행, 승인, 범위 검사, 재시도와 압축을 계속 관리합니다. 브리지는 모델의 한 단계만 스트리밍하고 도구 요청을 수집합니다. ScopeWeaver 도구나 OpenCode 기본 도구를 직접 실행하지 않습니다. 기존 API·OpenCode Go 공급자는 브리지 없이 사용할 수 있습니다. + +## 실행 + +Bun **1.3.14**가 검증된 CI 런타임입니다. 이후 버전은 아직 검증하지 않았습니다. + +```sh +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +브리지 토큰은 **공백이 아닌 문자 32자 이상**이어야 합니다. native의 비공개 CLI 서버는 호출마다 별도로 생성한 비밀번호(`OPENCODE_PASSWORD`)와 HTTP Basic 인증을 사용합니다. 이 비밀번호는 브리지 토큰 및 하위 공급자 키와 서로 다릅니다. + +ScopeWeaver 공급자 프로필에서 `opencode-sdk`, 주소 `http://127.0.0.1:4097`, 위 **브리지 토큰**을 입력합니다. 클라우드 API 키가 아닙니다. 모델을 조회한 뒤 정확한 `providerID/modelID`를 선택하세요. Go·Console API 키를 브리지 토큰으로 재사용하지 마세요. + +기본 런타임은 로그인하지 않은 상태에서 격리된 카탈로그와 공개 모델 목록을 사용합니다. 조회는 공개 카탈로그 GET 요청만 수행하며 **모델 추론을 실행하지 않습니다**. 현재 목록에 있고 고정 SDK가 인식하는 무료 OpenCode 모델만 표시합니다. 조회 실패는 오류로 반환합니다. 목록에 있다고 사용 자격·할당량·추론 성공이 보장되지는 않습니다. 무료 모델은 변경되거나 사라질 수 있으며 기본 설정에 유료 모델·인증정보 대체 경로는 없습니다. + +**2026년 10월 25일(KST)의 이전 검증:** `opencode/deepseek-v4.1-flash-free`로 실제 ScopeWeaver 대화를 완료했습니다. 제품의 native CLI·클라이언트 2.1.4 브리지를 통해 실제 추론을 세 번 호출해 UI 연결 테스트, 파일에 영향을 주기 전에 검토·승인한 호스트 도구 요청, 최종 화면의 답변 `FREE_OK`를 확인했습니다. 기본 무료 카탈로그에서 모델을 선택했으며 API 키, 계정 가져오기, 모델·응답 모의 데이터, 식별 위장이나 유료 대체 경로를 사용하지 않았습니다. 기본 도구는 계속 차단했고 호스트 동작은 ScopeWeaver가 실행했습니다. + +native 추론은 고정한 실제 CLI와 공식 클라이언트 API를 사용하고 추론 없는 모델 조회는 내장 SDK를 사용합니다. 앞선 내장 SDK의 수집 전용 경로는 HTTP 403 `FreeTierError`로 거부되었습니다. legacy의 실제 무료 접근은 아직 검증하지 않았습니다. 무료 모델과 서비스 사용 자격은 바뀔 수 있으며 이번 날짜의 검증으로 수치화된 할당량이나 이후의 사용 가능성을 보장하지 않습니다. 구조화된 거부는 `free_tier_restricted`로 전달하며 유료 모델로 자동 전환하지 않습니다. + +포트는 `SCOPEWEAVER_OPENCODE_PORT`로 변경할 수 있습니다. 기본값은 4097입니다. `SCOPEWEAVER_OPENCODE_HOST`는 `127.0.0.1`만 허용합니다. 브라우저 `Origin` 요청과 루프백이 아닌 주소를 거부하며 모든 경로에 토큰 인증이 필요합니다. + +호출 시간 제한은 `SCOPEWEAVER_OPENCODE_TIMEOUT_MS`로 **0~86,400,000ms** 사이에서 설정합니다. 기본값 **0**은 호스트가 취소할 때까지 호출을 허용하며 숨겨진 2분 추론 제한이 없습니다. 취소·종료 시에는 별도의 **1.5초** 강제 종료 제한이 적용됩니다. 공개 카탈로그 네트워크 조회는 10초 제한입니다. 요청은 **16 MiB**, 반환되는 도구 인자 JSON 하나는 **2,000,000자**, 네이티브 모델의 텍스트·사고·인자 합계는 **8,000,000자**, 워커 출력은 **20,000,000바이트**로 제한합니다. 넘으면 오류를 반환하며 데이터를 잘라서 전달하지 않습니다. + +## 전용 공급자 설정 + +명시적인 OpenAI 호환 공급자나 로컬 테스트 모델을 연결하려면 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG`에 운영자가 관리하는 JSON 파일 경로를 지정합니다. 파일은 체크아웃 밖에 두거나 Git에서 제외되는 `provider-config.local.json` 이름을 사용하세요. JSON 예시는 [영문 안내](README.md#dedicated-provider-configuration)에 있습니다. 예시 모델 ID는 `local/my-model`입니다. + +```json +{ + "providers": { + "local": { + "name": "Local model", + "package": "@opencode/ai/providers/openai-compatible", + "settings": { + "baseURL": "http://127.0.0.1:8000/v1", + "apiKey": "explicit-provider-key-if-needed" + }, + "models": { + "my-model": { + "name": "My model", + "capabilities": { "tools": true }, + "limit": { "context": 200000, "output": 32000 }, + "compatibility": { "maxTokensField": "max_tokens" } + } + } + } + } +} +``` + +이 파일을 지정하면 기본 모델 조회 대신 파일에 명시한 모델만 사용합니다. 사용자·프로젝트 OpenCode 설정, `.env`, 외부 플러그인, MCP 서버, 저장된 인증정보를 불러오지 않습니다. 해당 공급자에는 파일에 명시한 `apiKey`만 전달합니다. 원격 공급자는 추론 비용을 청구할 수 있으며 설정을 추가한다고 무료가 되지는 않습니다. 임의 SDK 패키지와 지원하지 않는 설정 필드를 거부합니다. + +## 실행 경계 + +각 호출은 환경 변수를 허용 목록으로 제한한 별도 Bun 워커와 임시 홈·설정·데이터 경로를 사용합니다. native는 실제 CLI의 비공개 서버, 실제 CLI `run`, 임시 DB, 한 번만 사용할 수 있는 인증된 루프백 추론 전송 서버를 소유합니다. 공식 클라이언트가 모델·세션 조회, 이벤트와 중단을 처리합니다. 클라우드 인증정보·사용자 인증 파일·프록시·사전 로드 설정을 상속하지 않습니다. 정확히 일치하는 공식 선택 의존성의 바이너리를 직접 사용하므로 `--ignore-scripts`가 유효하며 전역 설치가 필요 없습니다. 선택 의존성을 생략하지 마세요. + +native는 OpenCode의 실제 기본 도구 정의와 원래 구현을 유지합니다. 기본 도구 권한은 모두 ASK이고 CLI `--auto`는 사용하지 않습니다. 별도의 공개 `execute.before` 정책 훅이 생성된 `execute`를 포함한 모든 비호스트 도구를 실행 전에 거부합니다. 기본 도구를 요청하면 브리지 요청을 실패시키며 ScopeWeaver로 전달하지 않습니다. 등록한 호스트 수집 함수만 대기했다가 호스트 요청이 되고 다른 에이전트는 제거합니다. `sw_tool_N` 별칭으로 호스트 도구가 기본 도구를 덮어쓰지 않게 하며 설명과 반환값에 정확한 원래 이름을 보존합니다. + +호스트 system 구간은 두 줄바꿈으로 연결해 공개 사용자 지정 에이전트 system 설정에 그대로 넣습니다. OpenCode가 생성한 모델·환경·도구 문맥도 추가로 유지합니다. 마지막 정책 구간은 설명이 `Host tool:`로 시작하는 함수만 사용할 수 있음을 알립니다. 호스트 대화 역할, 도구 ID·결과, 호출별 제한·설정은 공개 context 훅으로 보존합니다. system 배열에 호스트 구간만 존재한다는 보장은 하지 않습니다. thinking·effort 필드는 공급자에 전달하며 지원하지 않는 설정은 명시적으로 실패합니다. 모델의 출력 한도가 작으면 그 한도까지 제한하고 입력·출력·캐시 사용량은 분리합니다. + +워커는 두 번째 모델 요청을 차단합니다. 비공개 전송 서버는 실제 CLI가 만든 요청을 원래 공급자 URL로 보내되 리디렉션을 거부하며 모델·주소·식별·인증정보를 대체하지 않습니다. 호스트 도구 요청은 중단, 세션 삭제, 소유 프로세스 정리가 끝난 뒤에만 전달합니다. 연결 종료·DELETE·시간 초과·서버 종료는 POSIX 워커 프로세스 그룹을 종료하며 1.5초 뒤 강제 종료합니다. 이 프로세스 트리 정리 경계가 구현되지 않아 **Windows에서는 native와 legacy 추론을 모두 거부**합니다. + +CLI의 공개 설정은 시스템·MDM 설정 탐색을 완전히 끌 수 없습니다. 따라서 macOS의 `/Library/Application Support/opencode`, 사용자·전역 `/Library/Managed Preferences` 아래 `ai.opencode.managed.plist`, Linux의 `/etc/opencode`가 있으면 native를 시작하지 않고 `configuration_error`로 실패합니다. 접근할 수 없는 경로와 끊어진 심볼릭 링크도 거부합니다. 해당 설정을 읽거나 변경하지 않습니다. + +native 모드는 완료·스트림 응답과 호스트 도구 기록 재생에서 64비트 정수를 포함한 도구 인자의 원본 JSON을 보존합니다. JavaScript로 파싱한 도구 인자를 실행하지 않습니다. 원본 도구 인자 밖에서 안전하게 표현할 수 없는 숫자, 지원하지 않는 콘텐츠, 알 수 없는 도구, 중복 ID, 잘못된 응답은 거부합니다. 스키마의 인라인 정의, 로컬 JSON Pointer `$ref`·`$defs`·유니온을 지원하며 원격 참조, `$id`, `$dynamicRef`, 해결되지 않는 참조는 거부합니다. 네이티브 초기화 시 자체 공급자·카탈로그 코드는 로드되며 **운영체제 샌드박스는 아닙니다**. 루프백 브리지도 원격 모델을 사용할 수 있어 ScopeWeaver의 로컬 전용 추론 조건을 충족하지 않습니다. + +## 런타임 버전과 구형 경로의 제한 + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | 고정한 버전 조합 | 기능 응답 `runtime` | +| --- | --- | --- | +| `native`(기본값) | 공식 CLI·`@opencode/client`·SDK **2.1.4** | `native` | +| `legacy` | `@opencode-ai/sdk/v2`·CLI **1.19.4** | `legacy` | + +구형 API를 선택하려면 시작 전에 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy`를 지정합니다. 두 조합은 로컬 **macOS arm64**에서 확인했으며 CI는 Linux에서 실제 런타임 테스트를 실행하도록 구성되어 있습니다. 다른 SDK 버전은 추론 전에 거부합니다. 모든 버전·플랫폼의 호환성을 보장하지 않습니다. + +구형 경로는 정확히 일치하는 공식 선택 의존성의 플랫폼 바이너리를 직접 실행하므로 `--ignore-scripts` 설치를 사용할 수 있습니다. 초기화되지 않은 래퍼나 전역 `opencode`는 실행하지 않습니다. 선택 의존성을 생략하지 마세요. CLI와 루프백 MCP 수집 서버는 요청 워커의 POSIX 프로세스 그룹과 함께 종료합니다. 이 정리 경계가 구현되지 않은 **Windows에서는 legacy를 거부**합니다. 호출마다 독립된 플러그인 패키지 복사본과 잠금 파일을 사용하며 공유 의존성에 쓰기 가능한 링크를 만들지 않습니다. + +legacy 정수의 절대값은 **9,007,199,254,740,991** 이하여야 합니다. + +구형 경로는 구조가 유효하고 안전하게 표현할 수 있는 JSON·기록을 지원합니다. 공백이나 같은 값을 나타내는 HTML·Unicode 이스케이프는 정규화될 수 있어 원본 JSON 표기 자체는 보장하지 않습니다. 안전하게 표현할 수 없는 정수, 불투명하거나 서명된 사고 기록, 지원하지 않는 공급자 덮어쓰기는 명시적으로 실패합니다. 원본 64비트 인자는 native 모드를 사용하세요. 구형 사용자 지정 공급자는 위의 OpenAI 호환 설정을 사용하되 `max_tokens` 호환 필드, 지원되는 도구 기능 필드, 컨텍스트·출력 한도만 매핑합니다. 지원하지 않는 `reasoningField`·기능 덮어쓰기를 거부합니다. SDK 경계 밖의 입력에는 기존 API·Go 프로필을 계속 사용할 수 있습니다. + +## 프로토콜과 검증 + +프로토콜 **`scopeweaver-opencode-sdk`, 버전 1**의 인증 경로는 `GET /v1/capabilities`, `GET /v1/models`, `POST /v1/complete`, `POST /v1/stream`, `DELETE /v1/requests/{request_id}`입니다. 스트림은 NDJSON이며 실제 네이티브 텍스트·사고 델타, 수집한 도구 요청, 사용량을 전달하고 성공 시 `message_stop`을 정확히 한 번 보냅니다. 오류는 공급자 원문을 노출하지 않는 제한된 코드·메시지를 사용합니다. 명시적인 구조화 응답으로 확인된 잔액·할당량 소진은 `quota_exhausted`(완료 경로 HTTP 402)로 구분하며 일반 속도 제한·인증·5xx 오류를 할당량 오류로 바꾸지 않습니다. 구조화된 모델 컨텍스트 초과는 `context_length_exceeded`(HTTP 400)로 전달해 호스트의 압축 신호를 보존하며 일반 400 응답이나 메시지 키워드만으로 분류하지 않습니다. 구조화된 `FreeTierError`는 `free_tier_restricted`(HTTP 403)로 구분하며 할당량·일반 금지 응답과 섞지 않습니다. 브리지는 대체 모델을 선택하지 않으며 호스트의 기존 정책이 다음 동작을 결정합니다. + +```json +{ + "request_id": "unique-call-uuid", + "session_id": "stable-host-conversation-uuid", + "model": "local/my-model", + "system": ["Host system instructions"], + "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }], + "tools": [], + "max_tokens": 64, + "thinking": "disabled" +} +``` + +요청에는 `request_id`, `model`, `system` 배열, `messages` 배열, `tools` 배열, `max_tokens`가 필요합니다. 선택 필드는 `session_id`, `temperature`, 문자열 배열 `stop`, `thinking`, `thinking_type`, `reasoning_effort`입니다. 메시지는 `text`, 어시스턴트의 `thinking`·`tool_use`, 텍스트 결과를 담은 사용자의 `tool_result` 블록을 사용합니다. 도구 선언은 `name`, `description`, `input_schema`를 사용합니다. 완료 응답은 `message`, `stop_reason`(`end_turn`, `tool_use`, `max_tokens`), 입력·출력·캐시 읽기·캐시 쓰기 수치를 분리한 `usage`를 포함합니다. 공개 JSON 예시는 [영문 프로토콜](README.md#protocol-and-checks), 검증·제한은 [protocol.mjs](src/protocol.mjs), 전송 처리는 [bridge.mjs](src/bridge.mjs)에서 확인할 수 있습니다. + +`opencode-go` 공급자에서는 `session_id`로 호출별 SDK 세션이 달라도 `x-opencode-session`을 유지합니다. 없으면 `request_id`를 사용합니다. `opencode`는 실제 런타임이 생성한 세션·식별 헤더를 그대로 유지합니다. 일반 공급자에는 게이트웨이 세션 헤더를 추가하지 않으며 실제 CLI의 User-Agent를 유지합니다. + +```sh +bun run check +bun test --timeout 30000 +``` + +테스트는 고정한 실제 CLI·클라이언트·SDK와 로컬 SSE 모델을 사용해 추론 없는 조회, 기록·설정, 도구 수집 안전성, 스트리밍·사용량, 원본 숫자 인자, 악성 기본 도구 요청, 동시 호출 격리와 취소를 확인합니다. 실제 무료 모델의 사용 자격을 증명하지는 않습니다. + +## ScopeWeaver 프로필과 다운그레이드 경계 + +빈 토큰은 같은 SDK 주소에서만 저장된 값을 유지합니다. 주소나 형식을 바꾸면 저장된 토큰을 재사용하지 않으며 `localhost`, `127.0.0.1`, `::1`은 서로 다른 자격 증명 범위입니다. SDK 템플릿은 저장하지 않은 클라우드 키, 프록시, 사용자 지정 세션 헤더를 지우고 모델을 빈칸으로 둡니다. 조회·추론·저장·활성화는 수행하지 않습니다. **모델 불러오기**는 명시적으로 실행하고, **연결 테스트**는 클릭할 때만 추론합니다. + +프로필 데이터베이스의 `CHECK` 제약에 `opencode-sdk`가 추가됩니다. SDK 프로필이 남아 있으면 이전 바이너리는 시작할 수 없습니다. 다운그레이드 전에 데이터베이스를 백업하고 현재 버전에서 이전에 지원하던 형식으로 변환하거나 SDK 프로필을 제거하세요. 이전 마이그레이션 검증에는 임시 데이터베이스를 사용했으며 운영 데이터베이스를 마이그레이션하지 않았습니다. [제공자 안내](../../docs/ko/llm-providers.md#opencode-sdk-bridge)를 참고하세요. diff --git a/adapters/opencode-sdk/README.md b/adapters/opencode-sdk/README.md new file mode 100644 index 00000000..ce478b43 --- /dev/null +++ b/adapters/opencode-sdk/README.md @@ -0,0 +1,124 @@ +# OpenCode SDK bridge + +[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md) + +Opt-in, loopback-only bridge for **native OpenCode CLI/client/SDK 2.1.4** and the separately tested **legacy SDK/CLI 1.19.4** pair. ScopeWeaver keeps its own conversation, tools, approvals, scope checks, retries, and compaction. The bridge streams a single model step and captures tool requests; it never runs ScopeWeaver tools or OpenCode's built-in tools. Existing API and OpenCode Go providers do not require this bridge. + +## Start + +Use Bun **1.3.14**, the tested CI runtime. Newer versions have not been verified: + +```sh +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +The bridge token must contain at least **32 non-whitespace characters**. The private native CLI server uses a separately generated per-call password (`OPENCODE_PASSWORD`) and HTTP Basic authentication. That password is distinct from the bridge token and upstream provider keys. + +In the ScopeWeaver provider profile, select `opencode-sdk`, set the endpoint to `http://127.0.0.1:4097`, and enter that **bridge token**. It is not a cloud API key. Discover models, then select an exact `providerID/modelID`. Do not reuse an OpenCode Go/Console token as the bridge token. + +The default signed-out runtime uses its own isolated catalog and public model discovery. Discovery makes public catalog GET requests, **never inference**. It exposes only currently listed zero-price OpenCode models that the pinned runtime recognizes. Catalog failures fail explicitly. A listed model does not prove available quota, eligibility, or successful inference; free models can change or disappear. No paid model or credential fallback is enabled by default. + +**Prior verification on October 25, 2026 (KST):** `opencode/deepseek-v4.1-flash-free` completed a real ScopeWeaver conversation through the production native CLI/client 2.1.4 bridge. Three actual inference calls covered the UI connection test, a host tool request reviewed and approved before its file effect, and the final visible answer `FREE_OK`. The model came from the default zero-price catalog; the run used no API keys, account imports, model/response fixtures, spoofed identities or paid fallback. Native tools remained blocked and ScopeWeaver owned the host action. + +Native inference uses the actual pinned CLI and official client APIs; inference-free discovery uses the embedded SDK. The earlier embedded capture-only SDK route received HTTP 403 `FreeTierError`. Legacy hosted free access remains unverified. Free models and service eligibility can change; this dated test establishes no numeric quota or continuing availability. `free_tier_restricted` reports a structured refusal without selecting a paid fallback. + +`SCOPEWEAVER_OPENCODE_PORT` changes port 4097. `SCOPEWEAVER_OPENCODE_HOST` must remain `127.0.0.1`. Browser `Origin` requests and non-loopback URLs are rejected. Protect the token like a password. All API routes require it. + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` is an optional whole-call deadline, from **0 to 86,400,000 ms**. Default **0** leaves call duration under the host's cancellation control; there is no implicit two-minute model cutoff. Cancellation and shutdown still have a separate **1.5-second** hard-stop bound for an owned worker/process group. Public catalog discovery has a 10-second network deadline. Requests are limited to **16 MiB**, individual returned tool-argument JSON to **2,000,000 characters**, combined native model text/reasoning/arguments to **8,000,000 characters**, and worker output to **20,000,000 bytes**. Exceeding a bound fails explicitly and never truncates data. + +## Dedicated provider configuration + +For an explicit custom OpenAI-compatible model or a local fixture, set `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` to a JSON file owned by the bridge operator. Keep it outside the checkout, or use the ignored name `provider-config.local.json`: + +```json +{ + "providers": { + "local": { + "name": "Local model", + "package": "@opencode/ai/providers/openai-compatible", + "settings": { + "baseURL": "http://127.0.0.1:8000/v1", + "apiKey": "explicit-provider-key-if-needed" + }, + "models": { + "my-model": { + "name": "My model", + "capabilities": { "tools": true }, + "limit": { "context": 200000, "output": 32000 }, + "compatibility": { "maxTokensField": "max_tokens" } + } + } + } + } +} +``` + +Select `local/my-model`. This file replaces default discovery for that bridge. It does not load OpenCode's user/project config, `.env` files, plugins, MCP servers, or saved credentials. Only the explicit `apiKey` is provided to that provider. A custom remote endpoint can charge for inference; configuration does not make it free. Arbitrary SDK packages and extra configuration keys are rejected. + +## Runtime boundary + +Every call runs in a separate Bun worker with an allowlisted environment and owned temporary home/config/data paths. Native owns a private official CLI server, an actual CLI `run`, its temporary database, and a one-use authenticated loopback egress transport. The official client supplies discovery, session events, and interruption. No worker inherits cloud credentials, user auth files, proxies, or preload settings. Pinned optional platform binaries are used directly; `--ignore-scripts` remains valid and no global install is needed. Do not omit optional dependencies. + +Native retains OpenCode's genuine tool definitions and original implementations. Every native permission is ASK and CLI `--auto` is absent. A public `execute.before` policy hook independently refuses every non-host tool before execution, including generated `execute`; a proposed native call fails the bridge request and never reaches ScopeWeaver. Only registered host capture functions may suspend and become host tool requests. Other agents are removed. Collision-free `sw_tool_N` aliases prevent host tools from replacing native definitions; descriptions retain exact original names and descriptions, and responses restore original host names. + +Host system segments use the public custom-agent system setting verbatim, joined by two newlines. OpenCode's generated model/environment/tool context is additionally retained. A final truthful policy part tells the model that only functions marked `Host tool:` are enabled. Host conversation roles, tool IDs/results, and per-call limits/settings use the public context hook. This preserves the host's messages while adding native runtime context; it does not promise a system array containing only the host segments. Model-specific thinking/effort fields are forwarded; unsupported provider settings fail explicitly. A smaller declared model output limit caps the requested limit. Native usage components and cache counts remain separate. + +The worker blocks any second model request. Its private egress transport forwards the genuine generated request to the original upstream URL with redirects disabled; it does not choose a different endpoint, model, identity, or credential. Host calls become visible only after interruption, session deletion, and owned process cleanup. Disconnect, DELETE, timeout, and shutdown terminate the POSIX worker process group, with a 1.5-second hard stop. Both native and legacy inference are rejected on Windows because that process-tree cleanup boundary is not implemented. + +The CLI public surface cannot disable all system/MDM configuration discovery. Native refuses to start when an unmanaged source exists: `/Library/Application Support/opencode` or an `ai.opencode.managed.plist` under the user's/global `/Library/Managed Preferences` on macOS, or `/etc/opencode` on Linux. Inaccessible sources and broken symlinks also fail closed with `configuration_error`. It never reads or changes those sources. + +In native mode, raw tool-argument JSON, including 64-bit integers, is preserved in completion, stream responses, and replayed host tool history. The parsed JavaScript projection is never executed. Unsafe numbers outside authoritative raw tool arguments, unsupported content, unknown native tool calls, duplicate tool IDs, and malformed responses fail closed. Tool schemas support inline definitions, local JSON Pointer `$ref`/`$defs`, and unions; remote references, `$id`, `$dynamicRef`, and unresolved references are rejected. Native initialization still loads its own built-in provider/catalog code; this is an application boundary, **not an operating-system sandbox**. A loopback bridge can use a remote model, so it does not satisfy ScopeWeaver's local-only inference policy. + +## Runtime versions and legacy limits + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | Pinned pair | Capability `runtime` | +| --- | --- | --- | +| `native` (default) | official CLI, `@opencode/client`, and SDK **2.1.4** | `native` | +| `legacy` | `@opencode-ai/sdk/v2` and CLI **1.19.4** | `legacy` | + +Set `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` before starting to choose the older API. Both pairs were tested locally on **macOS arm64**; CI is configured to run their actual-runtime fixtures on Linux. Other SDK versions are rejected before inference. This is not a universal cross-version or platform guarantee. + +Legacy uses the matching official optional platform binary directly. `--ignore-scripts` therefore remains valid: it does not invoke the uninitialized wrapper or a global `opencode` installation. Do not omit optional dependencies. Its owned CLI and loopback MCP capture server are terminated with the request worker's POSIX process group. **Legacy is rejected on Windows**, where that cleanup boundary is not implemented. Each CLI call uses its own private plugin package copy and lock, with no writable link into shared dependencies. + +Legacy integers must have an absolute value no greater than **9,007,199,254,740,991**. + +Legacy supports structurally valid, safely representable JSON/history. Whitespace and equivalent HTML/Unicode escapes may normalize; it does not promise lexical JSON fidelity. Unsafe parsed integers, opaque/signed reasoning history, and unsupported provider overrides fail explicitly; choose native mode for raw 64-bit argument fidelity. Legacy custom providers use the same explicit OpenAI-compatible configuration above, but only `max_tokens` compatibility, supported tool capability fields, and context/output limits are mapped. Unsupported `reasoningField` and capability overrides are rejected. Existing API/Go profiles remain available for inputs outside either SDK mode's tested boundary. + +## Protocol and checks + +Protocol **`scopeweaver-opencode-sdk`, version 1** has authenticated routes: `GET /v1/capabilities`, `GET /v1/models`, `POST /v1/complete`, `POST /v1/stream`, `DELETE /v1/requests/{request_id}`. Streaming uses NDJSON with actual native text/thinking deltas, captured tool-use blocks, terminal usage, and exactly one `message_stop` on success. Errors have bounded machine codes and redact provider messages. Explicit structured upstream billing/quota exhaustion is returned as `quota_exhausted` (HTTP 402 for complete); generic rate limits, auth errors, and 5xx responses are never relabeled as quota. Structured model context overflow is returned as `context_length_exceeded` (HTTP 400), preserving the host's compaction signal; generic 400 responses and message keywords are not enough. Structured `FreeTierError` is returned as `free_tier_restricted` (HTTP 403), separate from quota and generic forbidden responses. The bridge does not select a fallback model; the host's configured policy decides what happens next. + +Example completion request (the stream endpoint accepts the same body): + +```json +{ + "request_id": "unique-call-uuid", + "session_id": "stable-host-conversation-uuid", + "model": "local/my-model", + "system": ["Host system instructions"], + "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }], + "tools": [], + "max_tokens": 64, + "thinking": "disabled" +} +``` + +Optional fields are `session_id`, `temperature`, `stop` (strings), `thinking`, `thinking_type`, and `reasoning_effort`. Host content blocks are `text`, assistant `thinking`/`tool_use`, and user `tool_result` with text content. Tool declarations use `name`, `description`, and `input_schema`. Complete responses contain `message`, `stop_reason` (`end_turn`, `tool_use`, or `max_tokens`), and `usage` with separate input/output/cache-read/cache-write token counts. See [validation and serialization](src/protocol.mjs) and [transport](src/bridge.mjs) for the public wire contract and limits. + +For `opencode-go`, `session_id` keeps `x-opencode-session` stable across fresh per-call SDK sessions; otherwise `request_id` is used. `opencode` preserves its genuine runtime-generated session and identity headers. Generic providers receive no gateway-specific session header. The genuine CLI User-Agent is preserved. + +```sh +bun run check +bun test --timeout 30000 +``` + +Tests drive the **actual pinned CLI/client/SDK pair** against local SSE model fixtures. They verify inference-free discovery, host history/settings, capture safety, native streaming/usage, raw numeric arguments, hostile native calls, concurrent isolation, and cancellation. They do not prove real hosted free-model entitlement. See [한국어](README.ko.md). + +## ScopeWeaver profile and downgrade boundary + +A blank token retains a saved credential only for the same SDK endpoint. Changing endpoint or format must not reuse the saved token; `localhost`, `127.0.0.1`, and `::1` are distinct credential scopes. The SDK template clears unsaved cloud credentials, proxy and custom session header, leaves the model blank, and does not discover, infer, save or activate. Use **Load models** explicitly; **Test connection** performs inference only when clicked. + +The profile database `CHECK` constraint adds `opencode-sdk`. Older binaries cannot start while SDK profiles remain. Before downgrading, back up the database and use the current version to convert SDK profiles to an older supported format or remove them. Prior migration verification used disposable databases; it did not migrate a live database. See the [provider guide](../../docs/llm-providers.md#opencode-sdk-bridge). diff --git a/adapters/opencode-sdk/README.zh-Hans.md b/adapters/opencode-sdk/README.zh-Hans.md new file mode 100644 index 00000000..4591acf7 --- /dev/null +++ b/adapters/opencode-sdk/README.zh-Hans.md @@ -0,0 +1,122 @@ +# OpenCode SDK 桥接器 + +[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md) + +可选的仅限回环地址的桥接器,支持固定的 **OpenCode 原生 CLI/客户端/SDK 2.1.4**,以及单独测试的 **旧版 SDK/CLI 1.19.4**。ScopeWeaver 继续管理对话、工具执行、审批、范围检查、重试与压缩。桥接器只流式传送一次模型步骤并收集工具请求,不执行 ScopeWeaver 工具或 OpenCode 内置工具。现有 API 与 OpenCode Go 供应商无需此桥接器。 + +## 启动 + +已测试的 CI 运行时是 **Bun 1.3.14**;更新版本尚未验证。 + +```sh +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +桥接令牌必须至少包含 **32 个非空白字符**。原生私有 CLI 服务器使用每次调用独立生成的密码(`OPENCODE_PASSWORD`)和 HTTP Basic 认证;该密码、桥接令牌与上游供应商密钥彼此独立。 + +在 ScopeWeaver 供应商配置中选择 `opencode-sdk`,地址设为 `http://127.0.0.1:4097`,填入上述**桥接令牌**。它不是云端 API 密钥;不要复用 OpenCode Go/Console 令牌。读取模型后选择准确的 `providerID/modelID`。 + +默认运行时不登录账户,使用隔离的目录与公开模型发现。发现只发送公开目录 GET 请求,**不执行推理**;仅显示当前公开列表中、固定运行时识别的零价格 OpenCode 模型。目录错误会明确返回。列入目录不代表具备资格、可用配额或推理成功;免费模型可能改变或消失。默认不启用付费模型或凭证回退。 + +**2026-10-25(KST)的先前验证:** `opencode/deepseek-v4.1-flash-free` 曾通过产品的原生 CLI/客户端 2.1.4 桥接器完成真实 ScopeWeaver 对话。三次实际推理覆盖界面连接测试、文件产生影响前经审核批准的宿主工具请求,以及界面最终答案 `FREE_OK`。模型来自默认零价格目录,未使用 API 密钥、账户导入、模型/响应模拟数据、身份伪装或付费回退。原生工具保持阻止状态,宿主操作由 ScopeWeaver 执行。这是该日期的既有证据,不是本次重跑结果。 + +原生推理使用真正的固定版本 CLI 和官方客户端 API,无推理的模型发现使用内嵌 SDK。此前仅收集工具的内嵌 SDK 路径收到 HTTP 403 `FreeTierError`。旧版的真实托管免费访问尚未验证。模型及服务资格可能变化;该日期的测试不保证数值配额或持续可用性。结构化拒绝返回 `free_tier_restricted`,不切换到付费模型。 + +`SCOPEWEAVER_OPENCODE_PORT` 可修改默认端口 4097;`SCOPEWEAVER_OPENCODE_HOST` 必须为 `127.0.0.1`。拒绝浏览器 `Origin` 请求及非回环 URL。所有 API 路由均要求令牌;请像密码一样保护它。 + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` 的完整调用时限为 **0–86,400,000 毫秒**;默认 **0** 由宿主控制取消,没有隐藏的两分钟推理限制。取消与关闭另有 **1.5 秒**强制终止期限。公开目录请求限制为 **10 秒**。请求体上限 **16 MiB**,单个工具参数 JSON 上限 **2,000,000 字符**,原生模型文本/思考/参数合计上限 **8,000,000 字符**,工作进程输出上限 **20,000,000 字节**。超限明确失败,不截断数据。 + +## 专用供应商配置 + +自定义 OpenAI 兼容模型或本地测试模型可通过 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` 指定运营者管理的 JSON 文件。放在检出目录之外,或使用 Git 忽略的名称 `provider-config.local.json`: + +```json +{ + "providers": { + "local": { + "name": "Local model", + "package": "@opencode/ai/providers/openai-compatible", + "settings": { + "baseURL": "http://127.0.0.1:8000/v1", + "apiKey": "explicit-provider-key-if-needed" + }, + "models": { + "my-model": { + "name": "My model", + "capabilities": { "tools": true }, + "limit": { "context": 200000, "output": 32000 }, + "compatibility": { "maxTokensField": "max_tokens" } + } + } + } + } +} +``` + +选择 `local/my-model`。文件会替代该桥接器的默认模型发现,不载入用户/项目的 OpenCode 配置、`.env`、插件、MCP 服务器或已保存凭证。仅将明确配置的 `apiKey` 传给对应供应商。远程地址可能收取推理费用;添加配置不会使其免费。任意 SDK 软件包和额外配置字段均被拒绝。 + +## 运行边界 + +每次调用使用独立的 Bun 工作进程、允许列表中的环境变量及自有临时 home/配置/数据路径。原生模式拥有私有官方 CLI 服务器、实际 CLI `run`、临时数据库和一次性认证的回环出口传输服务;官方客户端处理发现、会话事件和中断。不继承云端凭证、用户认证文件、代理或预加载设置。直接使用固定的可选平台二进制,因此支持 `--ignore-scripts`,无需全局安装;**不要省略可选依赖**。 + +保留 OpenCode 的真实内置工具定义及原始实现。所有原生权限为 ASK,CLI 不使用 `--auto`。公开的 `execute.before` 策略钩子独立阻止所有非宿主工具在执行前运行,包括生成的 `execute`。原生工具请求使桥接请求失败,不会变成 ScopeWeaver 工具请求。只有注册的宿主收集函数可暂停并成为宿主工具请求;移除其他代理。无冲突的 `sw_tool_N` 别名防止宿主工具覆盖内置定义,描述保留原始名称与内容,响应恢复原始宿主名称。 + +宿主 system 片段以两个换行连接,原样写入公开的自定义代理 system 设置;同时保留 OpenCode 生成的模型/环境/工具上下文。最后的策略说明只有标记 `Host tool:` 的函数可用。通过公开 context 钩子保留宿主角色、工具 ID/结果及每次调用的限制/设置,不保证 system 数组只含宿主片段。按模型传递 thinking/effort 字段,不支持的设置明确失败;模型声明的较小输出限额会限制请求上限。输入、输出及缓存用量分开记录。 + +阻止第二次模型请求。私有出口将实际 CLI 生成的请求转发到原始上游 URL,拒绝重定向,不替换地址、模型、身份或凭证。只有中断、删除会话及清理自有进程完成后才返回宿主工具请求。断连、DELETE、超时和关闭终止 POSIX 工作进程组,并在 **1.5 秒**后强制终止。该进程树清理边界未在 Windows 实现,**原生和旧版推理均不支持 Windows**。 + +CLI 的公开设置不能完全关闭系统/MDM 配置发现。macOS 上存在 `/Library/Application Support/opencode` 或用户/全局 `/Library/Managed Preferences` 下的 `ai.opencode.managed.plist`,或 Linux 上存在 `/etc/opencode`,原生模式便以 `configuration_error` 拒绝启动。无法访问的路径和损坏符号链接也明确拒绝;不读取或修改这些来源。 + +原生模式在完成响应、流响应和宿主工具历史重放中保留原始工具参数 JSON,包括 64 位整数;不执行 JavaScript 解析后的投影。原始参数之外的不安全数值、不支持的内容、未知原生工具、重复工具 ID 和格式错误响应均拒绝。工具 schema 支持内联定义、本地 JSON Pointer `$ref`/`$defs` 与联合类型;拒绝远程引用、`$id`、`$dynamicRef` 和无法解析的引用。初始化仍会加载原生供应商/目录代码;这是应用边界,**不是操作系统沙箱**。回环桥接器仍可调用远程模型,因此不满足 ScopeWeaver 的仅本地推理策略。 + +## 版本与旧版限制 + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定组合 | 能力响应 `runtime` | +| --- | --- | --- | +| `native`(默认) | 官方 CLI、`@opencode/client`、SDK **2.1.4** | `native` | +| `legacy` | `@opencode-ai/sdk/v2`、CLI **1.19.4** | `legacy` | + +启动前设置 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` 可选择旧 API。两个组合的先前本地实际运行验证范围为 **macOS arm64**;Linux CI 已配置实际运行测试,但托管运行结果尚待验证。其他 SDK 版本在推理前被拒绝,不保证任意版本或平台兼容。 + +旧版直接运行匹配的官方可选平台二进制,支持 `--ignore-scripts`,不调用未初始化的包装器或全局 `opencode`。不要省略可选依赖。其 CLI 与回环 MCP 收集服务器随请求工作进程的 POSIX 进程组终止;Windows 不支持此清理边界。每次调用使用独立插件软件包副本及锁文件,不建立到共享依赖的可写链接。 + +旧版只支持结构有效、可安全表示的 JSON/历史。空白及等价 HTML/Unicode 转义可能被规范化,不承诺原始 JSON 字节。拒绝绝对值超过 **9,007,199,254,740,991** 的不安全整数、不透明/带签名的思考历史及不支持的供应商覆盖设置;需要原始 64 位参数保真时使用原生模式。旧版自定义供应商使用上面的显式 OpenAI 兼容配置,但只映射 `max_tokens` 兼容字段、支持的工具能力字段及上下文/输出限额;拒绝不支持的 `reasoningField` 和能力覆盖。超出任一 SDK 模式边界时,可继续使用 API/Go 配置。 + +## 协议与验证 + +协议为 **`scopeweaver-opencode-sdk`,版本 1**。认证路由:`GET /v1/capabilities`、`GET /v1/models`、`POST /v1/complete`、`POST /v1/stream`、`DELETE /v1/requests/{request_id}`。流使用 NDJSON,传递真实原生文本/思考增量、收集的工具请求及最终用量;成功时只发送一次 `message_stop`。错误使用有限机器代码并隐藏供应商消息。只有明确结构化的计费/配额耗尽才返回 `quota_exhausted`(完成请求 HTTP 402);普通限流、认证及 5xx 不会改标为配额错误。结构化上下文溢出返回 `context_length_exceeded`(HTTP 400),保留宿主压缩信号;普通 400 或消息关键词不足以分类。结构化 `FreeTierError` 返回 `free_tier_restricted`(HTTP 403),与配额及普通禁止响应分开。桥接器不选择后备模型;后续行为由宿主配置的策略决定。 + +完成请求示例(流端点接受相同正文): + +```json +{ + "request_id": "unique-call-uuid", + "session_id": "stable-host-conversation-uuid", + "model": "local/my-model", + "system": ["Host system instructions"], + "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }], + "tools": [], + "max_tokens": 64, + "thinking": "disabled" +} +``` + +可选字段为 `session_id`、`temperature`、字符串数组 `stop`、`thinking`、`thinking_type`、`reasoning_effort`。宿主内容块支持 `text`、助手 `thinking`/`tool_use` 及带文本结果的用户 `tool_result`。工具声明使用 `name`、`description`、`input_schema`。完成响应包含 `message`、`stop_reason`(`end_turn`、`tool_use` 或 `max_tokens`)及分离输入/输出/缓存读取/缓存写入计数的 `usage`。公开契约与限额见 [protocol.mjs](src/protocol.mjs) 和 [bridge.mjs](src/bridge.mjs)。 + +对于 `opencode-go`,`session_id` 使每次调用的新 SDK 会话保持相同 `x-opencode-session`;缺省时使用 `request_id`。`opencode` 保留真实运行时生成的会话及身份头部。普通供应商不会收到网关专用会话头部。保留真实 CLI 的 User-Agent。 + +```sh +bun run check +bun test --timeout 30000 +``` + +测试以固定的实际 CLI/客户端/SDK 和本地 SSE 模型验证无推理发现、宿主历史/设置、收集安全性、流/用量、原始数值参数、恶意原生调用、并发隔离及取消;不证明真实托管免费模型的使用资格。 + +## ScopeWeaver 配置与降级边界 + +空令牌仅在相同 SDK 地址保留已保存值。更改地址或格式不能复用旧令牌;`localhost`、`127.0.0.1`、`::1` 是不同凭证作用域。SDK 模板清除未保存的云端密钥、代理和自定义会话头部,模型留空,不发现、不推理、不保存、不启用。明确点击**读取模型**才发现模型;**测试连接**仅在点击时执行推理。 + +配置数据库 `CHECK` 约束增加 `opencode-sdk`;仍有 SDK 配置时,旧二进制无法启动。降级前备份数据库,并在当前版本中转换为旧版本支持的格式或移除 SDK 配置。此前迁移验证只使用临时数据库,没有迁移生产数据库。详见[供应商指南](../../docs/zh-Hans/llm-providers.md#opencode-sdk-bridge)。 diff --git a/adapters/opencode-sdk/README.zh-Hant.md b/adapters/opencode-sdk/README.zh-Hant.md new file mode 100644 index 00000000..a15dd232 --- /dev/null +++ b/adapters/opencode-sdk/README.zh-Hant.md @@ -0,0 +1,122 @@ +# OpenCode SDK 橋接器 + +[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md) + +可選的僅限迴路地址的橋接器,支持固定的 **OpenCode 原生 CLI/客戶端/SDK 2.1.4**,以及單獨測試的 **舊版 SDK/CLI 1.19.4**。ScopeWeaver 繼續管理對話、工具執行、審批、範圍檢查、重試與壓縮。橋接器只流式傳送一次模型步驟並收集工具請求,不執行 ScopeWeaver 工具或 OpenCode 內建工具。現有 API 與 OpenCode Go 供應商無需此橋接器。 + +## 啓動 + +已測試的 CI 運行時是 **Bun 1.3.14**;更新版本尚未驗證。 + +```sh +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +橋接權杖必須至少包含 **32 個非空白字元**。原生私有 CLI 伺服器使用每次呼叫獨立產生的密碼(`OPENCODE_PASSWORD`)與 HTTP Basic 驗證;該密碼、橋接權杖與上游供應商金鑰彼此獨立。 + +在 ScopeWeaver 供應商設定中選擇 `opencode-sdk`,地址設為 `http://127.0.0.1:4097`,填入上述**橋接權杖**。它不是雲端 API 密鑰;不要復用 OpenCode Go/Console 權杖。讀取模型後選擇準確的 `providerID/modelID`。 + +預設運行時不登錄賬戶,使用隔離的目錄與公開模型發現。發現只發送公開目錄 GET 請求,**不執行推理**;僅顯示當前公開列表中、固定運行時識別的零價格 OpenCode 模型。目錄錯誤會明確返回。列入目錄不代表具備資格、可用配額或推理成功;免費模型可能改變或消失。預設不啓用付費模型或憑證回退。 + +**2026-10-25(KST)的先前驗證:** `opencode/deepseek-v4.1-flash-free` 曾通過產品的原生 CLI/客戶端 2.1.4 橋接器完成真實 ScopeWeaver 對話。三次實際推理覆蓋界面連接測試、文件產生影響前經審核批准的宿主工具請求,以及界面最終答案 `FREE_OK`。模型來自預設零價格目錄,未使用 API 密鑰、賬戶導入、模型/響應模擬資料、身份偽裝或付費回退。原生工具保持阻止狀態,宿主操作由 ScopeWeaver 執行。這是該日期的既有證據,不是本次重跑結果。 + +原生推理使用真正的固定版本 CLI 和官方客戶端 API,無推理的模型發現使用內嵌 SDK。此前僅收集工具的內嵌 SDK 路徑收到 HTTP 403 `FreeTierError`。舊版的真實托管免費訪問尚未驗證。模型及服務資格可能變化;該日期的測試不保證數值配額或持續可用性。結構化拒絕返回 `free_tier_restricted`,不切換到付費模型。 + +`SCOPEWEAVER_OPENCODE_PORT` 可修改預設端口 4097;`SCOPEWEAVER_OPENCODE_HOST` 必須為 `127.0.0.1`。拒絕瀏覽器 `Origin` 請求及非迴路 URL。所有 API 路由均要求權杖;請像密碼一樣保護它。 + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` 的完整調用時限為 **0–86,400,000 毫秒**;預設 **0** 由宿主控制取消,沒有隱藏的兩分鐘推理限制。取消與關閉另有 **1.5 秒**強制終止期限。公開目錄請求限制為 **10 秒**。請求體上限 **16 MiB**,單個工具參數 JSON 上限 **2,000,000 字元**,原生模型文本/思考/參數合計上限 **8,000,000 字元**,工作進程輸出上限 **20,000,000 位元組**。超限明確失敗,不截斷資料。 + +## 專用供應商設定 + +自定義 OpenAI 兼容模型或本地測試模型可通過 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` 指定運營者管理的 JSON 文件。放在簽出目錄之外,或使用 Git 忽略的名稱 `provider-config.local.json`: + +```json +{ + "providers": { + "local": { + "name": "Local model", + "package": "@opencode/ai/providers/openai-compatible", + "settings": { + "baseURL": "http://127.0.0.1:8000/v1", + "apiKey": "explicit-provider-key-if-needed" + }, + "models": { + "my-model": { + "name": "My model", + "capabilities": { "tools": true }, + "limit": { "context": 200000, "output": 32000 }, + "compatibility": { "maxTokensField": "max_tokens" } + } + } + } + } +} +``` + +選擇 `local/my-model`。文件會替代該橋接器的預設模型發現,不載入用戶/項目的 OpenCode 設定、`.env`、插件、MCP 服務器或已保存憑證。僅將明確設定的 `apiKey` 傳給對應供應商。遠程地址可能收取推理費用;添加設定不會使其免費。任意 SDK 套件和額外設定字段均被拒絕。 + +## 運行邊界 + +每次調用使用獨立的 Bun 工作進程、允許列表中的環境變量及自有臨時 home/設定/資料路徑。原生模式擁有私有官方 CLI 服務器、實際 CLI `run`、臨時資料庫和一次性認證的迴路出口傳輸服務;官方客戶端處理髮現、工作階段事件和中斷。不繼承雲端憑證、用戶認證文件、代理或預載入設置。直接使用固定的可選平台二進制,因此支持 `--ignore-scripts`,無需全局安裝;**不要省略可選依賴**。 + +保留 OpenCode 的真實內建工具定義及原始實現。所有原生權限為 ASK,CLI 不使用 `--auto`。公開的 `execute.before` 策略鈎子獨立阻止所有非宿主工具在執行前運行,包括生成的 `execute`。原生工具請求使橋接請求失敗,不會變成 ScopeWeaver 工具請求。只有註冊的宿主收集函數可暫停並成為宿主工具請求;移除其他代理。無衝突的 `sw_tool_N` 別名防止宿主工具覆蓋內建定義,描述保留原始名稱與內容,響應恢復原始宿主名稱。 + +宿主 system 片段以兩個換行連接,原樣寫入公開的自定義代理 system 設置;同時保留 OpenCode 生成的模型/環境/工具上下文。最後的策略說明只有標記 `Host tool:` 的函數可用。通過公開 context 鈎子保留宿主角色、工具 ID/結果及每次調用的限制/設置,不保證 system 數組只含宿主片段。按模型傳遞 thinking/effort 字段,不支持的設置明確失敗;模型聲明的較小輸出限額會限制請求上限。輸入、輸出及快取用量分開記錄。 + +阻止第二次模型請求。私有出口將實際 CLI 生成的請求轉發到原始上游 URL,拒絕重定向,不替換地址、模型、身份或憑證。只有中斷、刪除工作階段及清理自有進程完成後才返回宿主工具請求。斷連、DELETE、逾時和關閉終止 POSIX 工作進程組,並在 **1.5 秒**後強制終止。該進程樹清理邊界未在 Windows 實現,**原生和舊版推理均不支持 Windows**。 + +CLI 的公開設置不能完全關閉系統/MDM 設定發現。macOS 上存在 `/Library/Application Support/opencode` 或用戶/全局 `/Library/Managed Preferences` 下的 `ai.opencode.managed.plist`,或 Linux 上存在 `/etc/opencode`,原生模式便以 `configuration_error` 拒絕啓動。無法訪問的路徑和損壞符號鏈接也明確拒絕;不讀取或修改這些來源。 + +原生模式在完成響應、流響應和宿主工具歷史重放中保留原始工具參數 JSON,包括 64 位整數;不執行 JavaScript 解析後的投影。原始參數之外的不安全數值、不支持的內容、未知原生工具、重復工具 ID 和格式錯誤響應均拒絕。工具 schema 支持內聯定義、本地 JSON Pointer `$ref`/`$defs` 與聯合類型;拒絕遠程引用、`$id`、`$dynamicRef` 和無法解析的引用。初始化仍會載入原生供應商/目錄代碼;這是應用邊界,**不是操作系統沙箱**。迴路橋接器仍可調用遠程模型,因此不滿足 ScopeWeaver 的僅本地推理策略。 + +## 版本與舊版限制 + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定組合 | 能力響應 `runtime` | +| --- | --- | --- | +| `native`(預設) | 官方 CLI、`@opencode/client`、SDK **2.1.4** | `native` | +| `legacy` | `@opencode-ai/sdk/v2`、CLI **1.19.4** | `legacy` | + +啓動前設置 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` 可選擇舊 API。兩個組合的先前本地實際運行驗證範圍為 **macOS arm64**;Linux CI 已設定實際運行測試,但托管運行結果尚待驗證。其他 SDK 版本在推理前被拒絕,不保證任意版本或平台兼容。 + +舊版直接運行匹配的官方可選平台二進制,支持 `--ignore-scripts`,不調用未初始化的包裝器或全局 `opencode`。不要省略可選依賴。其 CLI 與迴路 MCP 收集服務器隨請求工作進程的 POSIX 進程組終止;Windows 不支持此清理邊界。每次調用使用獨立插件套件副本及鎖文件,不建立到共享依賴的可寫鏈接。 + +舊版只支持結構有效、可安全表示的 JSON/歷史。空白及等價 HTML/Unicode 轉義可能被規範化,不承諾原始 JSON 位元組。拒絕絕對值超過 **9,007,199,254,740,991** 的不安全整數、不透明/帶簽名的思考歷史及不支持的供應商覆蓋設置;需要原始 64 位參數保真時使用原生模式。舊版自定義供應商使用上面的顯式 OpenAI 兼容設定,但只映射 `max_tokens` 兼容字段、支持的工具能力字段及上下文/輸出限額;拒絕不支持的 `reasoningField` 和能力覆蓋。超出任一 SDK 模式邊界時,可繼續使用 API/Go 設定。 + +## 協議與驗證 + +協議為 **`scopeweaver-opencode-sdk`,版本 1**。認證路由:`GET /v1/capabilities`、`GET /v1/models`、`POST /v1/complete`、`POST /v1/stream`、`DELETE /v1/requests/{request_id}`。流使用 NDJSON,傳遞真實原生文本/思考增量、收集的工具請求及最終用量;成功時只發送一次 `message_stop`。錯誤使用有限機器代碼並隱藏供應商消息。只有明確結構化的計費/配額耗盡才返回 `quota_exhausted`(完成請求 HTTP 402);普通限流、認證及 5xx 不會改標為配額錯誤。結構化上下文溢出返回 `context_length_exceeded`(HTTP 400),保留宿主壓縮信號;普通 400 或消息關鍵詞不足以分類。結構化 `FreeTierError` 返回 `free_tier_restricted`(HTTP 403),與配額及普通禁止響應分開。橋接器不選擇後備模型;後續行為由宿主設定的策略決定。 + +完成請求示例(流端點接受相同正文): + +```json +{ + "request_id": "unique-call-uuid", + "session_id": "stable-host-conversation-uuid", + "model": "local/my-model", + "system": ["Host system instructions"], + "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }], + "tools": [], + "max_tokens": 64, + "thinking": "disabled" +} +``` + +可選字段為 `session_id`、`temperature`、字元串數組 `stop`、`thinking`、`thinking_type`、`reasoning_effort`。宿主內容塊支持 `text`、助手 `thinking`/`tool_use` 及帶文本結果的用戶 `tool_result`。工具聲明使用 `name`、`description`、`input_schema`。完成響應包含 `message`、`stop_reason`(`end_turn`、`tool_use` 或 `max_tokens`)及分離輸入/輸出/快取讀取/快取寫入計數的 `usage`。公開契約與限額見 [protocol.mjs](src/protocol.mjs) 和 [bridge.mjs](src/bridge.mjs)。 + +對於 `opencode-go`,`session_id` 使每次調用的新 SDK 工作階段保持相同 `x-opencode-session`;缺省時使用 `request_id`。`opencode` 保留真實運行時生成的工作階段及身份標頭。普通供應商不會收到網關專用工作階段標頭。保留真實 CLI 的 User-Agent。 + +```sh +bun run check +bun test --timeout 30000 +``` + +測試以固定的實際 CLI/客戶端/SDK 和本地 SSE 模型驗證無推理髮現、宿主歷史/設置、收集安全性、流/用量、原始數值參數、惡意原生調用、並行隔離及取消;不證明真實托管免費模型的使用資格。 + +## ScopeWeaver 設定與降級邊界 + +空權杖僅在相同 SDK 地址保留已保存值。更改地址或格式不能復用舊權杖;`localhost`、`127.0.0.1`、`::1` 是不同憑證作用域。SDK 模板清除未保存的雲端密鑰、代理和自定義工作階段標頭,模型留空,不發現、不推理、不保存、不啓用。明確點擊**讀取模型**才發現模型;**測試連接**僅在點擊時執行推理。 + +設定資料庫 `CHECK` 約束增加 `opencode-sdk`;仍有 SDK 設定時,舊二進制無法啓動。降級前備份資料庫,並在當前版本中轉換為舊版本支持的格式或移除 SDK 設定。此前遷移驗證只使用臨時資料庫,沒有遷移生產資料庫。詳見[供應商指南](../../docs/zh-Hant/llm-providers.md#opencode-sdk-bridge)。 diff --git a/adapters/opencode-sdk/bun.lock b/adapters/opencode-sdk/bun.lock new file mode 100644 index 00000000..fa39b6f6 --- /dev/null +++ b/adapters/opencode-sdk/bun.lock @@ -0,0 +1,990 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "scopeweaver-opencode-sdk", + "dependencies": { + "@opencode-ai/plugin": "1.19.4", + "@opencode-ai/sdk": "1.19.4", + "@opencode/ai": "2.1.4", + "@opencode/cli": "2.1.4", + "@opencode/client": "2.1.4", + "@opencode/core": "2.1.4", + "@opencode/plugin": "2.1.4", + "@opencode/sdk": "2.1.4", + "@opencode/util": "2.1.4", + "effect": "4.0.1", + "jsonc-parser": "3.3.1", + "opencode-ai": "1.19.4", + }, + }, + }, + "packages": { + "@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqcCq2PiFY1dbK/0Ck45KuvE8jfdxRuuAE9Y5w46dAk6U+9vPOeg1CDcmR+ncqmrYrhRl3nmyDttyDahyjCzAw=="], + + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], + + "@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dXzrVsLR5f6tr+U04jq4AXoRroGFBTvODnLgss0SWbzNjGGQg3XqtQ9j7rCLo6o8qbYGuAHvqUrIpUCuiscuFg=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], + + "@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="], + + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="], + + "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], + + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1057.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.75", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-aJdScAJeeXC1cdMDCk8/3eDoHch8o0z+oxlqFALqBK2+hbtSz+hZQ9DhfarXJmva9+FM2gmC5PUJ8BB/HbAqXQ=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-YxmL1Ar7bV9ZkJvtv+lj+MF6le76NnZnbmgySyZuxpZaCv0/1lZ3KxKy6RIj1h9ui2hWnVjZxbmTMeO0y/NrQA=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.78", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-3VHknDRngoebsaqyT1iR75b5cBT2zfEtODK6yYmYltljkK1HEAQLs2FEd1IpWqAeOYdMGaG9wikngWLTufY7RA=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.21", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/credential-provider-env": "^3.972.76", "@aws-sdk/credential-provider-http": "^3.972.78", "@aws-sdk/credential-provider-login": "^3.972.83", "@aws-sdk/credential-provider-process": "^3.972.76", "@aws-sdk/credential-provider-sso": "^3.973.20", "@aws-sdk/credential-provider-web-identity": "^3.972.82", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-aLx9hbCX6mGAyv4ZZd8UagJ4RASHVJw6BFDSIyuKmLkqNL6H47EjBPK6qaJ4B5TDV6mc+vMa0SJFYyo4mkvVQg=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.83", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-9bJU6FjRacF4FK/QHf8Dyxhur7B1grRj8C1Yh4IkpoFW2exQflPdCv0VrbemLW9ZctLNle+AE7TETs9KNvuLYg=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.88", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.76", "@aws-sdk/credential-provider-http": "^3.972.78", "@aws-sdk/credential-provider-ini": "^3.973.21", "@aws-sdk/credential-provider-process": "^3.972.76", "@aws-sdk/credential-provider-sso": "^3.973.20", "@aws-sdk/credential-provider-web-identity": "^3.972.82", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-AG0KID5qhQPzrFkh5b0Cws1Amytvzo0L0Xj6hMgJXEwD6ZXAqninDhSNAWJiN5zU4YVVZnuidnqwPwYJ04IamA=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-pdhKFTXv1zPYhh92MTlp0jVbLWvUEV9v+rziuEkoskGGQ2iJskxHTO38nA8UAbT/N4IM3n8OZJ9rIY5IBO15/A=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.20", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/token-providers": "3.1155.0", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-DlxrAB3MnjNLx/da7b89W+3DyJQYso/q7Ns15MsST2IV2nkAYQOYTUCaffNSjgrpMP99Dm2mV2jxzcbyF94ZfA=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.82", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-LUo46IqPTJ4xKBsrLuoaxiv/D2GTBCcFDMD1y3jKIqPR/p7Rw14RJFZ0iRWftqZtyLPQX9JMGnSi3mBu4y/pNA=="], + + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1057.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1057.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-cognito-identity": "^3.972.38", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-rbrEHtz11g0kxsSkYr3fx2HABNNblp4AhB2MgPvJHgYOWfJ2eBviU7Mvoaef0PW8QH6lbZDfJcnM7eKvtvz3sw=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.49", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1155.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-iIcQnYLhWuq9qOLQbIWFrxF3PF0svVeZvHnUS4DUnO4zUFmFHIyGyjqrntXG+e/5PUgfavV0pYfDIKHxTzN2LA=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.10", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@babel/runtime": ["@babel/runtime@7.29.10", "", {}, "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA=="], + + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.1" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-MZ7M1a1dxMqTqeNwIGSECjO7RrReEgeEwHo8BnbOqP+8IfVpdiJi/iPCmkFVWlAxanMl4qhsq69e0Hkul9t+bA=="], + + "@effect/platform-node": ["@effect/platform-node@4.0.1", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.1", "undici": "^8.11.2" }, "peerDependencies": { "effect": "^4.0.1", "redis": ">=5.0.0 <7.0.0" } }, "sha512-FT0Fu5LO0/XP9zBpj0HqrtJNLluK4StNKqf6TSulUsjp8FPBsDVXGJiDuyoN5J2yVMT7tgvhxSwaDcObWVRIaQ=="], + + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.1", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.22.0" }, "peerDependencies": { "effect": "^4.0.1" } }, "sha512-VanaejudEhKJ4mpmqnEzdncVUhN6sGSO9ZDND54mRxAlZxj+4dYAWjDTYAy9o4tJo9aALeP2U8cVcOFzEdSeLg=="], + + "@ff-labs/fff-bin-android-arm64": ["@ff-labs/fff-bin-android-arm64@0.10.5", "", { "os": "android", "cpu": "arm64" }, "sha512-idn1m9ycwv0Dzz6ih4H8e/X1WpGF6kglUxqP78oYgoC2sgAdPtLxD6hIAZK13p8isK7H8EMwrMze/4lynEDcKQ=="], + + "@ff-labs/fff-bin-darwin-arm64": ["@ff-labs/fff-bin-darwin-arm64@0.10.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tG0VSt6fs3C2/Y6NDp57YJVXGEnrJ4F/e5gDrFCbNeuwJe9LmcCKh6oHWUEZOaaymYT90kQmwuXZTvORP3G6Vw=="], + + "@ff-labs/fff-bin-darwin-x64": ["@ff-labs/fff-bin-darwin-x64@0.10.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-pwdFJUB/oeO4S/SfmRDdE5ipbAMYoVtRMwTZ12Wn2H46PgNU5DWnSjHhUNTLuKmkQU/ZIJL5Sn9j/fzwuIzcoA=="], + + "@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-prbY72gR+VPoSxl1InglwSwVHM0kErYMjUOpdvjQhS8eRdXYL3hTuYQv9L7fiB0mqaDzcwFa339uMr3p8ujDrw=="], + + "@ff-labs/fff-bin-linux-arm64-musl": ["@ff-labs/fff-bin-linux-arm64-musl@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-MQN/CEKn4c4toooGCmZ6N8cP9mhgE4tnR6rr6a4k9JjH0xi05fYdG4+BabPkr+l/mDc01DsRpSKVNBmvoZ3IlQ=="], + + "@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-8NzFGGyqrFaCtb+SPTFd0xQGUlHCJMf0KYGwuca46IP37nN4OtDT1wnyZ9oaPDZ+2qqyxox10hCnqGlmj3PJzQ=="], + + "@ff-labs/fff-bin-linux-x64-musl": ["@ff-labs/fff-bin-linux-x64-musl@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mioRbn8aAW2Jmm4W0w3poePo65iSE+REfO+Jc0iqsjirImEy6pKt7dswj+7LSU6RPWwPsMb4POTMENwNLMlJww=="], + + "@ff-labs/fff-bin-win32-arm64": ["@ff-labs/fff-bin-win32-arm64@0.10.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GIZiea0AhuUDtJtXO2mGWs3wil1OR6lpLKOqXMW8EhHVHs8SB+xwzOgZ3Hg7YLEJm+Ui/2tFW/NLbqRuxpmtrQ=="], + + "@ff-labs/fff-bin-win32-x64": ["@ff-labs/fff-bin-win32-x64@0.10.5", "", { "os": "win32", "cpu": "x64" }, "sha512-EvCWb7wLrtCvkziksAnTVxcGa0roaWNC978t4nG3ESKEty/Mvrj8kaLAzqWfuA2Xk+gTW+qisJYxa4FqFnqR0Q=="], + + "@ff-labs/fff-bun": ["@ff-labs/fff-bun@0.10.5", "", { "optionalDependencies": { "@ff-labs/fff-bin-android-arm64": "0.10.5", "@ff-labs/fff-bin-darwin-arm64": "0.10.5", "@ff-labs/fff-bin-darwin-x64": "0.10.5", "@ff-labs/fff-bin-linux-arm64-gnu": "0.10.5", "@ff-labs/fff-bin-linux-arm64-musl": "0.10.5", "@ff-labs/fff-bin-linux-x64-gnu": "0.10.5", "@ff-labs/fff-bin-linux-x64-musl": "0.10.5", "@ff-labs/fff-bin-win32-arm64": "0.10.5", "@ff-labs/fff-bin-win32-x64": "0.10.5" }, "os": [ "!aix", "!sunos", "!freebsd", "!openbsd", ], "cpu": [ "x64", "arm64", ] }, "sha512-n4t5e4mzbEAXO6SyPHx8ajECGvfTZ/y4TNCqvKSwROpcTqfobaq2T+MdsfsKyA+j+B11NmpCVLjVhCTel8iB8Q=="], + + "@ff-labs/fff-node": ["@ff-labs/fff-node@0.10.5", "", { "dependencies": { "ffi-rs": "^1.0.0" }, "optionalDependencies": { "@ff-labs/fff-bin-android-arm64": "0.10.5", "@ff-labs/fff-bin-darwin-arm64": "0.10.5", "@ff-labs/fff-bin-darwin-x64": "0.10.5", "@ff-labs/fff-bin-linux-arm64-gnu": "0.10.5", "@ff-labs/fff-bin-linux-arm64-musl": "0.10.5", "@ff-labs/fff-bin-linux-x64-gnu": "0.10.5", "@ff-labs/fff-bin-linux-x64-musl": "0.10.5", "@ff-labs/fff-bin-win32-arm64": "0.10.5", "@ff-labs/fff-bin-win32-x64": "0.10.5" }, "os": [ "!aix", "!sunos", "!freebsd", "!openbsd", ], "cpu": [ "x64", "arm64", ] }, "sha512-O4KZoO9lZluTqdHUYgd23xpEX/u7G87qQTgUIgtJ4BFTh1x/p243/M6AU2HdSHYWK+vrMRAdDXDzeUW/9rjk3Q=="], + + "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], + + "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@isaacs/string-locale-compare": ["@isaacs/string-locale-compare@1.1.0", "", {}, "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ=="], + + "@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.12", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", "@lydell/node-pty-linux-x64": "1.2.0-beta.12", "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", "@lydell/node-pty-win32-x64": "1.2.0-beta.12" } }, "sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g=="], + + "@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ=="], + + "@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q=="], + + "@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw=="], + + "@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.12", "", { "os": "linux", "cpu": "x64" }, "sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ=="], + + "@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g=="], + + "@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.12", "", { "os": "win32", "cpu": "x64" }, "sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw=="], + + "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.3.1", "", { "dependencies": { "@modelcontextprotocol/core": "2.3.1", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.8", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-mIGZXpHsjnZ6lD+gD/WCMpR5k8yVQQ8nNFH1N0Srf7AvnwTUMYD6pvTY8ng2+He5FfV7YMZLmrjL7cLa/cc3dQ=="], + + "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.3.1", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g=="], + + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], + + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], + + "@npmcli/agent": ["@npmcli/agent@4.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg=="], + + "@npmcli/arborist": ["@npmcli/arborist@9.4.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/map-workspaces": "^5.0.0", "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^4.0.0", "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^5.0.0", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", "minimatch": "^10.0.3", "nopt": "^9.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.0", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "pacote": "^21.0.2", "parse-conflict-json": "^5.0.1", "proc-log": "^6.0.0", "proggy": "^4.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", "walk-up-path": "^4.0.0" }, "bin": { "arborist": "bin/index.js" } }, "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA=="], + + "@npmcli/config": ["@npmcli/config@10.8.1", "", { "dependencies": { "@npmcli/map-workspaces": "^5.0.0", "@npmcli/package-json": "^7.0.0", "ci-info": "^4.0.0", "ini": "^6.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "walk-up-path": "^4.0.0" } }, "sha512-MAYk9IlIGiyC0c9fnjdBSQfIFPZT0g1MfeSiD1UXTq2zJOLX55jS9/sETJHqw/7LN18JjITrhYfgCfapbmZHiQ=="], + + "@npmcli/fs": ["@npmcli/fs@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og=="], + + "@npmcli/git": ["@npmcli/git@7.0.2", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", "semver": "^7.3.5", "which": "^6.0.0" } }, "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg=="], + + "@npmcli/installed-package-contents": ["@npmcli/installed-package-contents@4.0.0", "", { "dependencies": { "npm-bundled": "^5.0.0", "npm-normalize-package-bin": "^5.0.0" }, "bin": { "installed-package-contents": "bin/index.js" } }, "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA=="], + + "@npmcli/map-workspaces": ["@npmcli/map-workspaces@5.0.3", "", { "dependencies": { "@npmcli/name-from-folder": "^4.0.0", "@npmcli/package-json": "^7.0.0", "glob": "^13.0.0", "minimatch": "^10.0.3" } }, "sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw=="], + + "@npmcli/metavuln-calculator": ["@npmcli/metavuln-calculator@9.0.3", "", { "dependencies": { "cacache": "^20.0.0", "json-parse-even-better-errors": "^5.0.0", "pacote": "^21.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5" } }, "sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg=="], + + "@npmcli/name-from-folder": ["@npmcli/name-from-folder@4.0.0", "", {}, "sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg=="], + + "@npmcli/node-gyp": ["@npmcli/node-gyp@5.0.0", "", {}, "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ=="], + + "@npmcli/package-json": ["@npmcli/package-json@7.0.5", "", { "dependencies": { "@npmcli/git": "^7.0.0", "glob": "^13.0.0", "hosted-git-info": "^9.0.0", "json-parse-even-better-errors": "^5.0.0", "proc-log": "^6.0.0", "semver": "^7.5.3", "spdx-expression-parse": "^4.0.0" } }, "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ=="], + + "@npmcli/promise-spawn": ["@npmcli/promise-spawn@9.0.1", "", { "dependencies": { "which": "^6.0.0" } }, "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q=="], + + "@npmcli/query": ["@npmcli/query@5.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-8TZWfTQOsODpLqo9SVhVjHovmKXNpevHU0gO9e+y4V4fRIOneiXy0u0sMP9LmS71XivrEWfZWg50ReH4WRT4aQ=="], + + "@npmcli/redact": ["@npmcli/redact@4.0.0", "", {}, "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q=="], + + "@npmcli/run-script": ["@npmcli/run-script@10.0.4", "", { "dependencies": { "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", "proc-log": "^6.0.0" } }, "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg=="], + + "@opencode-ai/plugin": ["@opencode-ai/plugin@1.19.4", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "1.19.4", "effect": "4.0.0-beta.83", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.4.5", "@opentui/keymap": ">=0.4.5", "@opentui/solid": ">=0.4.5" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-VN71q/OmfggxIhiDbuzYDYmZcQMsNyVSw1o91e7LD6WUn/9jx4usI9ZpoPbC5p/Prpo/Umzpd+38+EuLoKRr+A=="], + + "@opencode-ai/pty": ["@opencode-ai/pty@0.3.1", "", { "optionalDependencies": { "@opencode-ai/pty-darwin-arm64": "0.3.1", "@opencode-ai/pty-darwin-x64": "0.3.1", "@opencode-ai/pty-linux-arm64-gnu": "0.3.1", "@opencode-ai/pty-linux-arm64-musl": "0.3.1", "@opencode-ai/pty-linux-x64-gnu": "0.3.1", "@opencode-ai/pty-linux-x64-musl": "0.3.1", "@opencode-ai/pty-win32-arm64": "0.3.1", "@opencode-ai/pty-win32-x64": "0.3.1" }, "bin": { "opencode-pty": "bin/opencode-pty.js" } }, "sha512-xmxBVI1z/J6Jt3cC5Mlcc2iOxi5cFtPF3q+023sEZJMEE9hBqh7EdXKZesAzAJ2qBWXGEEqWTudnERY7suoAKw=="], + + "@opencode-ai/pty-darwin-arm64": ["@opencode-ai/pty-darwin-arm64@0.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pn3dqOX9F4HZ9Vzjb9UTm4B0dz5w9c8jBsy8/Dp1WzrLOjg91cjx/LzPqXG7rrzD39ygyxnHZE4694f7lh9kuQ=="], + + "@opencode-ai/pty-darwin-x64": ["@opencode-ai/pty-darwin-x64@0.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-uBzxgFUxfGew7yR60YW5ng8is571AHWBg4MqytwDVAwlmkEaAzIvja/nxD+dWaszOq+PtJIaYnNqf4ybmG1JXg=="], + + "@opencode-ai/pty-linux-arm64-gnu": ["@opencode-ai/pty-linux-arm64-gnu@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-/8GK/hfnHa4O5vvVuFOd/SMr07n19dLmoILjsn8BmTBlBEU5oZClssKoeCDiPigFsVuL2cMBTqb2bONuD5SyDg=="], + + "@opencode-ai/pty-linux-arm64-musl": ["@opencode-ai/pty-linux-arm64-musl@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jemE95Tw4tVPUpmHGqDtTKlSnxFexZT6+R05esxdT9uiVnflK9mR0fLlgZpsgSDEQIZPywo5scA4wgqZaPIWKw=="], + + "@opencode-ai/pty-linux-x64-gnu": ["@opencode-ai/pty-linux-x64-gnu@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-DfU49EkQrE/G+HQZmWg7jvwCTdOkYcFvXFts6MNRycnrtm/K7Qq0SKVGYmZC0J2LN4SmgqCUILzmS+rDNkyw8g=="], + + "@opencode-ai/pty-linux-x64-musl": ["@opencode-ai/pty-linux-x64-musl@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-P+/oP0Xkfjecd/NmD+dwYQE+l/cUjhevONBauzjfOWSqd32ltznEpKqbfJW0RldR+Gztr9sJdkAATjRHURX1RQ=="], + + "@opencode-ai/pty-win32-arm64": ["@opencode-ai/pty-win32-arm64@0.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-HM2t9xYnFU2NYbFowVINKiVzbTdRtnIf/1kPC5lulLcqWf5g+N/O+uXy4dGqErdun49vJ+MP8EydG52whkE/xQ=="], + + "@opencode-ai/pty-win32-x64": ["@opencode-ai/pty-win32-x64@0.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xUpe+TApPY81AII9EXb9MdN2/WyDJB0t3C/vMsSxBQZSvh6mfML7mqt2kK9lQntQn5wz1kmKi05NRr9bHtkzoQ=="], + + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.19.4", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-3Wuc4t8gEuWKHbX1Na9oDK6PgOveid70l4Ks5RCjRIo2RcP8FQN3NS6Mg3XWc2+WiUfpfrmvBWlh5u1zeQfDQw=="], + + "@opencode/ai": ["@opencode/ai@2.1.4", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.1.4", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.1", "google-auth-library": "10.5.0" } }, "sha512-980bht1i8xv6hN/TQ+3tGujHuxfduje3jW69SKBvmiDxQBZ468Mk7sR4ztOy3NB/4fbI8iWEYhrcc3GbWWMxrw=="], + + "@opencode/cli": ["@opencode/cli@2.1.4", "", { "optionalDependencies": { "@opencode/cli-darwin-arm64": "2.1.4", "@opencode/cli-darwin-x64": "2.1.4", "@opencode/cli-darwin-x64-baseline": "2.1.4", "@opencode/cli-linux-arm64": "2.1.4", "@opencode/cli-linux-arm64-musl": "2.1.4", "@opencode/cli-linux-x64": "2.1.4", "@opencode/cli-linux-x64-baseline": "2.1.4", "@opencode/cli-linux-x64-baseline-musl": "2.1.4", "@opencode/cli-linux-x64-musl": "2.1.4", "@opencode/cli-windows-arm64": "2.1.4", "@opencode/cli-windows-x64": "2.1.4", "@opencode/cli-windows-x64-baseline": "2.1.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe", "opencode2": "bin/opencode.exe" } }, "sha512-vtHFa/1frSylpx1cZRuzmM2ecHWn6YB8cNvhw8eWKPvSFvDnckZXugDbi3LJYD9TGjRx2U6JRo725qwqtwMUfQ=="], + + "@opencode/cli-darwin-arm64": ["@opencode/cli-darwin-arm64@2.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QwjyvovTdT0gFi5rxUyPVlYePIe3ELduR9olu+KJWe089XVIDOTMhxylBe/ZGIpvoXHYo6bp2NskIiqCFAEXsw=="], + + "@opencode/cli-darwin-x64": ["@opencode/cli-darwin-x64@2.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-pdx0Iyl+IK1SdSQOhvz8JE4Rk0PGyacZ55FaxmyowYe9h0aFZNs3UAj7wNYod6Hsu4qsAeSVkdfQLr4SI8G1oA=="], + + "@opencode/cli-darwin-x64-baseline": ["@opencode/cli-darwin-x64-baseline@2.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-htb+MYexlbFPc/da+jXePmclbXuMMX4m8kSIe24wbgy/npTC8wv1VHj0QTDt9YttTnnas+Yr1kNmJ7j/86yt+Q=="], + + "@opencode/cli-linux-arm64": ["@opencode/cli-linux-arm64@2.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-drw0LWMG0niaE94ePXm1h7qjFBZZRcSZD3gact3hx3OYPXesp7xrOt0bn3sicYNUN6fojERrDKR6jYrXmZX+rA=="], + + "@opencode/cli-linux-arm64-musl": ["@opencode/cli-linux-arm64-musl@2.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-tAYpbC/cY6ospekIftWTkwRfuHRfgKeOfg98A3RPs/2cckmGrOhyhFeHol22H4se7NRAFPboMCj0+UV7Wgt6uQ=="], + + "@opencode/cli-linux-x64": ["@opencode/cli-linux-x64@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-o8EJ0ZsT4dzDKN+0mpG2EKbNGzJyp+F1++Av28UReRVfiEv+N34eZEnN/unkiMoerKHad6W6Ii7k4ukTnPNmNQ=="], + + "@opencode/cli-linux-x64-baseline": ["@opencode/cli-linux-x64-baseline@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-0/H4wRDAW4hA5IWN440XwDrRATbvT3cno+UUsqFynTu0TeGXNADjK2AZfvhNTfyJaVyCa+v7VhB+r7ghuMa5EQ=="], + + "@opencode/cli-linux-x64-baseline-musl": ["@opencode/cli-linux-x64-baseline-musl@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zpQqwKXFHMB4Qw83t5yT95EPV46eZ066UVjm6w3WPP+RRl5NQasLAkFdBfQsl86iXKfie7HNOx8rA5seZjCAkQ=="], + + "@opencode/cli-linux-x64-musl": ["@opencode/cli-linux-x64-musl@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-skzwj4raZAiDZKWgs1mjb3KRZGK1WlfBr4GEjAXAa3qQxnt+tAZ0rObU4T12foI99n1e+5COi4iHhl0qOkHqyA=="], + + "@opencode/cli-windows-arm64": ["@opencode/cli-windows-arm64@2.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZDKG+2rhCYb2ngl1B/H8Lx2ll9NjNLhghj10/War9eK5ASYysWvR+26+Bdv+/J29eZdzeXViJnOwf3tZBdMakA=="], + + "@opencode/cli-windows-x64": ["@opencode/cli-windows-x64@2.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-9h0Nb/3g6HxnFY6lawSqNSCxek0K3zUPjOAoF20pyPBaLMq65sXfWLVwgyS/21AM0NrUw77S9CeP0tPM+XrbGQ=="], + + "@opencode/cli-windows-x64-baseline": ["@opencode/cli-windows-x64-baseline@2.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-76bsf2uQK/vj2CI0p6dhten9ZD8HmGLeC4B1XfwEaD1xmWHB+8hwTGy5VVvHsoqH+flRQeq3Xfk0VCyQ9zA9+g=="], + + "@opencode/client": ["@opencode/client@2.1.4", "", { "dependencies": { "@opencode/protocol": "2.1.4", "@opencode/schema": "2.1.4" }, "peerDependencies": { "effect": "4.0.1", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-xNHP8Xu4wcq4G849Xnbb8IxYxMTzF0TqvohWVUQZH5aURrVHIIbHMfXbZY6s4i4C9kpVbwjXFFwWAo6KIK3oiw=="], + + "@opencode/codemode": ["@opencode/codemode@2.1.4", "", { "dependencies": { "acorn": "8.15.0", "effect": "4.0.1" } }, "sha512-1gFyokMI4EgFyu0NgaCPr/HPZQKasZuwCPX4v7Plm+dYANetRt98vv3BXG4pJpn4ERPI+mc86tsHfpulDdZEfA=="], + + "@opencode/core": ["@opencode/core@2.1.4", "", { "dependencies": { "@ai-sdk/cohere": "3.0.27", "@ai-sdk/perplexity": "3.0.26", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/vercel": "2.0.39", "@ff-labs/fff-bun": "0.10.5", "@ff-labs/fff-node": "0.10.5", "@lydell/node-pty": "1.2.0-beta.12", "@modelcontextprotocol/client": "2.3.1", "@modelcontextprotocol/core": "2.3.1", "@opencode-ai/pty": "0.3.1", "@opencode/ai": "2.1.4", "@opencode/codemode": "2.1.4", "@opencode/plugin": "2.1.4", "@opencode/plugin-browser": "2.1.4", "@opencode/schema": "2.1.4", "@opencode/util": "2.1.4", "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", "@standard-schema/spec": "1.1.0", "bun-pty": "0.4.11", "diff": "8.0.4", "drizzle-orm": "1.0.0-rc.5-169397b", "effect": "4.0.1", "fuzzysort": "3.1.0", "gitlab-ai-provider": "6.19.0", "gray-matter": "4.0.3", "htmlparser2": "8.0.2", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "immer": "11.1.4", "jose": "6.0.11", "jsonc-parser": "3.3.1", "mime-types": "3.0.2", "tree-sitter-bash": "0.25.0", "tree-sitter-powershell": "0.25.10", "web-tree-sitter": "0.25.10", "which": "6.0.1", "zod": "4.1.8" } }, "sha512-0DI8oiOCPgZb1PC20+aF8Ns3Jx7xxa85xf++ZmtdnLp7OpblUaeUnidE8wIDkm5CHT8CgAXdKw6gK4aDm3ta6A=="], + + "@opencode/plugin": ["@opencode/plugin@2.1.4", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.1.4", "@opencode/client": "2.1.4", "@opencode/protocol": "2.1.4", "@opencode/schema": "2.1.4", "@opencode/util": "2.1.4", "effect": "4.0.1" }, "peerDependencies": { "@opencode/theme": "2.1.4", "@opentui/core": ">=0.6.8", "@opentui/solid": ">=0.6.8", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-SGAkdGp0VRxos2BEkRiyRl75G7DtuvP0ywgiEiHz1PbdUwUe2EAURMrzFgLCYCpWEm/zaNrguxPDVL0carrCsA=="], + + "@opencode/plugin-browser": ["@opencode/plugin-browser@2.1.4", "", { "dependencies": { "@opencode/plugin": "2.1.4", "@opencode/schema": "2.1.4", "effect": "4.0.1" } }, "sha512-t7J4aaGrigJG/HiG7ya7UIU0yV9bnqmNdC9Gcrm4aJrBdNr9QyqUjPQSIdBDjIDW+35pSjnbQs1xTZ3j9hw84Q=="], + + "@opencode/protocol": ["@opencode/protocol@2.1.4", "", { "dependencies": { "@opencode/schema": "2.1.4", "effect": "4.0.1" } }, "sha512-dnifgvY6p4xdHkXRH3PEaobfCqrHb1AhihKxPNRNkGn5SueRCFCfK2pZW1GkeC36fGCmcLxlfihuIMaQL1BVzA=="], + + "@opencode/schema": ["@opencode/schema@2.1.4", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.1" } }, "sha512-PLr8eFW81Yz6U7YsSgfSUClUUH//8omh3HiXu/L08VX5arorWNAu2uSjoXyHIGkm7TaTbdXBGPmmDfpvrDM2wQ=="], + + "@opencode/sdk": ["@opencode/sdk@2.1.4", "", { "dependencies": { "@opencode/client": "2.1.4", "@opencode/core": "2.1.4", "@opencode/plugin": "2.1.4", "@opencode/schema": "2.1.4", "@opencode/server": "2.1.4", "@opencode/util": "2.1.4", "effect": "4.0.1" } }, "sha512-V2lxqZR0QbHVfyGoeaRtazI78rMOtErjq6UuIlY9Y3mtH3KenBFgI1v2xKyzFER43JDOIMxCEtRuFiWmRxpjRQ=="], + + "@opencode/server": ["@opencode/server@2.1.4", "", { "dependencies": { "@effect/platform-node": "4.0.1", "@effect/platform-node-shared": "4.0.1", "@opencode/core": "2.1.4", "@opencode/protocol": "2.1.4", "@opencode/schema": "2.1.4", "@opencode/simulation": "2.1.4", "@opencode/util": "2.1.4", "effect": "4.0.1" } }, "sha512-WYsp+pk0AkzG89bPqs2qTl0UfYiBdLxVlzPYGP8jChQxaPy41AyybkvhvGywyr0tCXTKJXxXgYLgEcAzYf+6bg=="], + + "@opencode/simulation": ["@opencode/simulation@2.1.4", "", { "dependencies": { "@opencode/ai": "2.1.4", "@opencode/core": "2.1.4", "@opencode/plugin": "2.1.4", "@opencode/protocol": "2.1.4", "@opencode/util": "2.1.4", "@opentui/core": "0.6.8", "effect": "4.0.1", "ws": "8.21.0" } }, "sha512-Vt0h1pnugzmrazPvkeAFUkDAlejQiHhUQrpqsmYBscxcFXBNNKQXDqfMsTktl4R7vCBWYZixGQMHobQm5s/bQg=="], + + "@opencode/util": ["@opencode/util@2.1.4", "", { "dependencies": { "@effect/opentelemetry": "4.0.1", "@effect/platform-node": "4.0.1", "@effect/platform-node-shared": "4.0.1", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "cross-spawn": "7.0.6", "effect": "4.0.1", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "open": "11.0.4", "pacote": "21.5.1" } }, "sha512-Ihj6DqbnnovgUYSXki4PgvGxNbmB1wggDySIdM55Rz+sTfwFZ+zwROh3PrC4a8Vx/cJ6xQg41Uxa4nbcTbGI4A=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.8.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-transformer": "0.219.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.8.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], + + "@opentui/core": ["@opentui/core@0.6.8", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.6.8", "@opentui/core-darwin-x64": "0.6.8", "@opentui/core-linux-arm64": "0.6.8", "@opentui/core-linux-arm64-musl": "0.6.8", "@opentui/core-linux-x64": "0.6.8", "@opentui/core-linux-x64-musl": "0.6.8", "@opentui/core-win32-arm64": "0.6.8", "@opentui/core-win32-x64": "0.6.8" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wjqLwLdYGy/6qEBTtZEifuwwVzt06o0BqBF02xJ46whGEUK/gy+h25+1GK4dE9LFnHphgsqeA31f7PHBJPp91w=="], + + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.6.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kmu5Rp5qC3StrGAzLvBu6SLEXd2Qq60Yf0YCmL7BmDgYt0QUZ4M2JsSS7BFg7crpk+4Yz17N1ZeOCPumE4s83w=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.6.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-eGJcssaONQ32+xYOTRCdGGXnsMJJJLn6mHYt0W79Lck8zlMuel5dWWlCbxdM3qvX/IEHr+f56bGoojTvJjDw6w=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.6.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-ji2LPeEX4RU6RKC0/l70hZWmqD0AvvMvnd3WvpZUNOeaIBFseSzE0QEl3apRPUqAoYiuL7uRgwSV4eewa+xXpg=="], + + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.6.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-b+3TI8rtFBQa7T04F7XVt1I8/eL2+GX0X6x9WbDBnNmolgf1DX3ZdVPmKEP8PlxAmDYB+gek+cQAwnkwiYl5YQ=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.6.8", "", { "os": "linux", "cpu": "x64" }, "sha512-g8hOxXpff7hOnZNlJRKCqWJr/dwVK/EyEhNsgNK4jLS13BUoaYWXi878xLXJ5acC82qEyGlYWL/kR6ACkiD0YA=="], + + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.6.8", "", { "os": "linux", "cpu": "x64" }, "sha512-eVQSjO5wXYiqunz/V92WweHvVTCV+LtZOnu0z6kYjwwbAvsnTxISOFAyA9H71Sy3tsibjiu1o+E27twDyM9HlA=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.6.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-P4llQH8kFeiknVp0kfTydKaJC95BSW4mf3fub9HMgFx49eiF3qgcctbebZN+/WaD8Pshu0wiN1KPitqDTyrIDw=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.6.8", "", { "os": "win32", "cpu": "x64" }, "sha512-6dZuSw883BDetY59G2QwWHJG1kwfPkL+lWhDQ3a6ICxW4/7nito3uPkMRaRDb8DiTHgx0wMt/RdiydjQmZd+lw=="], + + "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@redis/bloom": ["@redis/bloom@6.3.0", "", { "peerDependencies": { "@redis/client": "^6.3.0" } }, "sha512-NQ5poYpZr0jv6zazN1cy6JY5kIipKFZ3R9GTL4kOUTX3a9YAfY29U8bsE4lWhVQEM+G4V0BXVC57mubIdDijeA=="], + + "@redis/client": ["@redis/client@6.3.0", "", { "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-fHrjwGCBvAANGi+TuRuwKi83sIOzgHr8eh+Uf48w3fSX7Hhrz174NrWqfJhAC2vnN0d/tfaZpdr6+o6JC3QHwg=="], + + "@redis/json": ["@redis/json@6.3.0", "", { "peerDependencies": { "@redis/client": "^6.3.0" } }, "sha512-yt6vRfPBXtW/3qm53cF81TSQpLMjWdW4/2E9WQ2GgbvsKkuCKCFQIcMiKlil1ZMnezK+IQuS795xKU5psbaegw=="], + + "@redis/search": ["@redis/search@6.3.0", "", { "peerDependencies": { "@redis/client": "^6.3.0" } }, "sha512-RPXAZmKjZZG7Tm7m1w9keFdXzbL91PiSHQBfQRe9xwW40jNsTRbG0FwhIz4QlHPU4gKDAsCy8IQHj4yZl4H9rw=="], + + "@redis/time-series": ["@redis/time-series@6.3.0", "", { "peerDependencies": { "@redis/client": "^6.3.0" } }, "sha512-+fJOB8mN1z5WRakCJNh5lVdz1KqvHGSOistsKeitD24QsCtPwyRQXWC0iq6T/i/ubLE/YcguZqPm8F598MKk4A=="], + + "@sigstore/bundle": ["@sigstore/bundle@4.0.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A=="], + + "@sigstore/core": ["@sigstore/core@3.2.1", "", {}, "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g=="], + + "@sigstore/protobuf-specs": ["@sigstore/protobuf-specs@0.5.2", "", {}, "sha512-SQqvFMt4V78fdjcDdYX6HbiVSOR4QK3ZgwCa2KOsopAgPIHy1rU5UDUmzLl02r5oyyaYcYHR1hpwDRk/yUe+Mw=="], + + "@sigstore/sign": ["@sigstore/sign@4.1.1", "", { "dependencies": { "@gar/promise-retry": "^1.0.2", "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.0", "@sigstore/protobuf-specs": "^0.5.0", "make-fetch-happen": "^15.0.4", "proc-log": "^6.1.0" } }, "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ=="], + + "@sigstore/tuf": ["@sigstore/tuf@4.0.2", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", "tuf-js": "^4.1.0" } }, "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ=="], + + "@sigstore/verify": ["@sigstore/verify@3.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA=="], + + "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], + + "@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-nxu3SgmAw9JXT2CtkU0m/XNLWpP9MsaBx1zAGAypCbYj15tIFlmcYwpF+Oh18le83d+IM9PT7ENdXnE4C+d5mA=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-iq+cW3mAb7vfcxEEpYi3zXKpDtbrIFyanWjQl4zBq4seWD4OSxXDWSfespZxenX6aEaighn+NR3u1nU1DSvs3w=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tufjs/canonical-json": ["@tufjs/canonical-json@2.0.0", "", {}, "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA=="], + + "@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="], + + "@types/node": ["@types/node@26.6.5", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="], + + "@types/ws": ["@types/ws@8.18.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w=="], + + "@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.7", "", { "os": "android", "cpu": "arm64" }, "sha512-t6Wx3Xll6c07Nuk0k3xnZsxKFxlshm92i0U/BiTHc6kQbvu+fMJF+gKsj4yEj886jH51CM3EqZT9Xdhq9CdUVw=="], + + "@yuuang/ffi-rs-darwin-arm64": ["@yuuang/ffi-rs-darwin-arm64@1.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OueBlUFBT9IwD9pQnoYs0UszRBEySskfrEPXXfvKfGjL/DXnfn6kUheQ3oIP6sSmshVGNQUwrTCPo6feAa4QjA=="], + + "@yuuang/ffi-rs-darwin-x64": ["@yuuang/ffi-rs-darwin-x64@1.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-x4mxXOKwSgwYx6OBAKrW+Ocp8um2KoPpUwaD0Z2tmR6EekoRvFGN7s/eEK1avqCxcsLN7/MzGOnCCFyGpOVfPA=="], + + "@yuuang/ffi-rs-linux-arm-gnueabihf": ["@yuuang/ffi-rs-linux-arm-gnueabihf@1.3.7", "", { "os": "linux", "cpu": "arm" }, "sha512-9r//Z022QYVoIAY458Dpk8HWkisk/y4NJC3d52RlkdsLD+9p0zn0cKKSoMF/xUsOWDFSmOf+cwU/Xadi/wxFMQ=="], + + "@yuuang/ffi-rs-linux-arm64-gnu": ["@yuuang/ffi-rs-linux-arm64-gnu@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-nrW4MlFlyQInfxTsI5wtcIpbV4KlHLWo2BCk4CHIETlUT+PUspNFqDg+Byy7My3hpCZlXp0WsMVP/3N0LlybNw=="], + + "@yuuang/ffi-rs-linux-arm64-musl": ["@yuuang/ffi-rs-linux-arm64-musl@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-eLLx4P8DzNnuPif19nAvKPaf88lSf6KFbDetH3nAIRZvI495h0jigN835Ayr7uAADssohGfO9MPMH33DXmFErA=="], + + "@yuuang/ffi-rs-linux-x64-gnu": ["@yuuang/ffi-rs-linux-x64-gnu@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-upEz1Q98T51x2In872fsjgHCJbE3e8r9JxhJD+5NvSMk6BpieYWRodTbsMxV6oKWmfl12loKwF2j4OBSWKl0lA=="], + + "@yuuang/ffi-rs-linux-x64-musl": ["@yuuang/ffi-rs-linux-x64-musl@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-YOcOkIwVvIhpwuGJ428h15vpE0KnT3teFG0g2J31FrBxrVh9IH4HeTGXjelI3l9SI5gyiZDnD4PQnQ1U6oqTcA=="], + + "@yuuang/ffi-rs-win32-arm64-msvc": ["@yuuang/ffi-rs-win32-arm64-msvc@1.3.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZfYzLV1w7Mhzh9XRScEMpw9oazIEiwO+0+TYHHRL882d5oD0Q99YikmRhdifsoHCCQefKmWF/4BXCeFkzg8NFA=="], + + "@yuuang/ffi-rs-win32-ia32-msvc": ["@yuuang/ffi-rs-win32-ia32-msvc@1.3.7", "", { "os": "win32", "cpu": [ "x64", "ia32", ] }, "sha512-ifYz+f+giKpT39lKWDru+om6QnA9J7J953ns60xHgB46ESZvPd/Y2W5+PesRNC9U+vnFEiLZ9CNC+R3fBWC1pg=="], + + "@yuuang/ffi-rs-win32-x64-msvc": ["@yuuang/ffi-rs-win32-x64-msvc@1.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-H3s4wOLtZtKtmuPjUNE+bhnSKxxpXcuxG70jRC8dUMHqIm5iCvdoZ4XYDeq7/uUYlySerXFmRL4dAVCX/TUNTg=="], + + "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ansi-regex": ["ansi-regex@6.4.0", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + + "bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="], + + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="], + + "bun-pty": ["bun-pty@0.4.11", "", {}, "sha512-DpBfXCoWbcDziqL4gT3sFw/O6vfe2Gm1LmsnjfGjpqNhmsPjBcVorTrRv7OVATsy88PiXfn486vp0w4x3/Q84g=="], + + "bundle-name": ["bundle-name@4.1.1", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA=="], + + "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + + "cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], + + "content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "drizzle-orm": ["drizzle-orm@1.0.0-rc.5-169397b", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@drizzle-team/minipg": ">=0.3.2", "@effect/sql-d1": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-libsql": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-mysql2": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-pg": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-pglite": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-sqlite-bun": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-sqlite-do": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-sqlite-node": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-sqlite-wasm": ">=4.0.0-beta.105 || >=4.0.0", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=17", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.7.1", "@tursodatabase/database-common": ">=0.7.1", "@tursodatabase/database-wasm": ">=0.7.1", "@tursodatabase/serverless": ">=1.4.0", "@tursodatabase/sync": ">=0.7.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.105 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.2.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@drizzle-team/minipg", "@effect/sql-d1", "@effect/sql-libsql", "@effect/sql-mysql2", "@effect/sql-pg", "@effect/sql-pglite", "@effect/sql-sqlite-bun", "@effect/sql-sqlite-do", "@effect/sql-sqlite-node", "@effect/sql-sqlite-wasm", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@tursodatabase/serverless", "@tursodatabase/sync", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-xf5TudQhFplMMzjwgmMY5f64Sqbv4iwXxdTHBHqvLMt+SHyY8rAYhzNLQchEkDxiqMclTS/txPEOg5pdQy0Tcw=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "effect": ["effect@4.0.1", "", {}, "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "engine.io-client": ["engine.io-client@6.6.7", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-GyAvmPA7gnRCpDQbYACAy79Lg4ewevrmaPzySZz0OVvgyZc3gEfLLqPTYk8wVBScjOEpM9ZO4KryyFpVItjGEA=="], + + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], + + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + + "fast-check": ["fast-check@4.10.2", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "ffi-rs": ["ffi-rs@1.3.7", "", { "optionalDependencies": { "@yuuang/ffi-rs-android-arm64": "1.3.7", "@yuuang/ffi-rs-darwin-arm64": "1.3.7", "@yuuang/ffi-rs-darwin-x64": "1.3.7", "@yuuang/ffi-rs-linux-arm-gnueabihf": "1.3.7", "@yuuang/ffi-rs-linux-arm64-gnu": "1.3.7", "@yuuang/ffi-rs-linux-arm64-musl": "1.3.7", "@yuuang/ffi-rs-linux-x64-gnu": "1.3.7", "@yuuang/ffi-rs-linux-x64-musl": "1.3.7", "@yuuang/ffi-rs-win32-arm64-msvc": "1.3.7", "@yuuang/ffi-rs-win32-ia32-msvc": "1.3.7", "@yuuang/ffi-rs-win32-x64-msvc": "1.3.7" } }, "sha512-5MMZQS2t/6f/ec3sx0f51gBcKGZPGfolPAW04lvLOSYnr7gmOKs8YzH1Rh+34WGO+VtcgcTuxaUUoJLcR4Tirg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + + "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], + + "gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="], + + "gcp-metadata": ["gcp-metadata@8.1.4", "", { "dependencies": { "gaxios": "7.1.3", "google-logging-utils": "1.1.3", "json-bigint": "^1.0.0" } }, "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw=="], + + "get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="], + + "gitlab-ai-provider": ["gitlab-ai-provider@6.19.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-ym+p6PxKvCKfBE3VKYTiacJwsmgq/DrOuGctpBnbIBekdnZT31RM6yTTNdzVS83K3+Ym4M+qNY34HEcTVM6EEQ=="], + + "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], + + "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], + + "google-logging-utils": ["google-logging-utils@1.2.0", "", {}, "sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphql": ["graphql@16.14.2", "", {}, "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA=="], + + "graphql-request": ["graphql-request@6.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="], + + "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], + + "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], + + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], + + "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + + "http-cache-semantics": ["http-cache-semantics@4.3.0", "", {}, "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + + "ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="], + + "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + + "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], + + "ip-address": ["ip-address@10.7.5", "", {}, "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + + "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="], + + "js-yaml": ["js-yaml@3.15.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w=="], + + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@5.0.0", "", {}, "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ=="], + + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + + "json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="], + + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="], + + "just-diff-apply": ["just-diff-apply@5.5.0", "", {}, "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw=="], + + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + + "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], + + "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], + + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + + "minipass-fetch": ["minipass-fetch@5.0.2", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "optionalDependencies": { "iconv-lite": "^0.7.2" } }, "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ=="], + + "minipass-flush": ["minipass-flush@1.0.7", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minipass-sized": ["minipass-sized@2.0.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "msgpackr": ["msgpackr@2.1.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-JHAdwubWNEadHQsFAib6uXOzKFxY8QOWHu9LCCksOINMC+TjgaKS2Tdrw7dW5SY0LLz8YK2/YqLSi2vigQt05w=="], + + "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], + + "multipasta": ["multipasta@0.2.9", "", {}, "sha512-kAovZHgxryqb0xTFz7OTCPxbas8UBTTwCDhfrlaNecp1/z6xcMfp5PVhDxsW7jONHyDF10/ccp30MxA2+66GJw=="], + + "nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="], + + "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + + "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], + + "npm-bundled": ["npm-bundled@5.0.0", "", { "dependencies": { "npm-normalize-package-bin": "^5.0.0" } }, "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw=="], + + "npm-install-checks": ["npm-install-checks@8.0.0", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA=="], + + "npm-normalize-package-bin": ["npm-normalize-package-bin@5.0.0", "", {}, "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="], + + "npm-package-arg": ["npm-package-arg@13.0.2", "", { "dependencies": { "hosted-git-info": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^7.0.0" } }, "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA=="], + + "npm-packlist": ["npm-packlist@10.0.4", "", { "dependencies": { "ignore-walk": "^8.0.0", "proc-log": "^6.0.0" } }, "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng=="], + + "npm-pick-manifest": ["npm-pick-manifest@11.0.3", "", { "dependencies": { "npm-install-checks": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "npm-package-arg": "^13.0.0", "semver": "^7.3.5" } }, "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ=="], + + "npm-registry-fetch": ["npm-registry-fetch@19.1.1", "", { "dependencies": { "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", "npm-package-arg": "^13.0.0", "proc-log": "^6.0.0" } }, "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw=="], + + "open": ["open@11.0.4", "", { "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.1", "wsl-utils": "^1.0.0" } }, "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg=="], + + "openai": ["openai@6.49.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg=="], + + "opencode-ai": ["opencode-ai@1.19.4", "", { "optionalDependencies": { "opencode-darwin-arm64": "1.19.4", "opencode-darwin-x64": "1.19.4", "opencode-darwin-x64-baseline": "1.19.4", "opencode-linux-arm64": "1.19.4", "opencode-linux-arm64-musl": "1.19.4", "opencode-linux-x64": "1.19.4", "opencode-linux-x64-baseline": "1.19.4", "opencode-linux-x64-baseline-musl": "1.19.4", "opencode-linux-x64-musl": "1.19.4", "opencode-windows-arm64": "1.19.4", "opencode-windows-x64": "1.19.4", "opencode-windows-x64-baseline": "1.19.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe" } }, "sha512-O3rTc9OCd6CeeJohCAjFYD3MX5U0weKzKnySfsJ9sw9uAbNZZK4meIcjd+7k3inmZB5G2qONSen3yA45yECiFA=="], + + "opencode-darwin-arm64": ["opencode-darwin-arm64@1.19.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-31sy6eA88QO2vXra395rPxe995a2G0IKKAqm7B/ZESwIGVK9auMdYfKd7mf5icI72oFMLUwRJGQ1f4B/ykbDJg=="], + + "opencode-darwin-x64": ["opencode-darwin-x64@1.19.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ILgql0oUVdZeWnWVGsv5by/gPeY9ytYS5KTCQSoxgo0H1TzbnNMSd+IAn9dHL1H4XSZBtgPTfdMK9r1sWPbYbA=="], + + "opencode-darwin-x64-baseline": ["opencode-darwin-x64-baseline@1.19.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hy8iFxdW2CFbr842M8uzCO1bgCAEm6UIRgtSJKj19+a4UtZyS+PwbAat2R2rwIx+O6PqavzDas5DNE+dz0+K3A=="], + + "opencode-linux-arm64": ["opencode-linux-arm64@1.19.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bA/QpgwRgv20xVay0b4EGo3IjMQQl4GbODjhXWGUvovoJYYyFosueox2aLASxTbHK6oP1zpUxaZRFhgNrhhfgA=="], + + "opencode-linux-arm64-musl": ["opencode-linux-arm64-musl@1.19.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sn41KddzgT+UBXtwuOR7/4X4ww8xOIEaw1bS74X0cO+BBgj7EY28gSRN4DNSJatXdDRJhDvhmNebWxly/cfYCw=="], + + "opencode-linux-x64": ["opencode-linux-x64@1.19.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rQ/l3jmJoxf8ZYnBargg9alURlz6buEilysGODtzkcmcYtcZ91LOgF034oUUHHQl8/y1hbRTDvg6ujqUjQsMXw=="], + + "opencode-linux-x64-baseline": ["opencode-linux-x64-baseline@1.19.4", "", { "os": "linux", "cpu": "x64" }, "sha512-oU1Plu0b4x8DRdUHxjnEdQ/IR01THw6teGf3blNysJHKS5fqXztsHiFiy6sXD1Isne+1Oa7Wi0sysfXEaBrudQ=="], + + "opencode-linux-x64-baseline-musl": ["opencode-linux-x64-baseline-musl@1.19.4", "", { "os": "linux", "cpu": "x64" }, "sha512-HxG4Y5XlpyQ64tlbQ3PMTwtIXlOLEuhI2VZ9CfKdAsSvsfo2Nh2w0FzS1esuLXNvdaDaO1CWZ3p7wk1JrerSkg=="], + + "opencode-linux-x64-musl": ["opencode-linux-x64-musl@1.19.4", "", { "os": "linux", "cpu": "x64" }, "sha512-SW/DNSbM2fjLN2q1IfPHgWTNSpQ8TgXjRUVNLc1VWlgKROfizpPTtWmjGWmxie2nZVuBcSVw/RlQE3ulgA8vVA=="], + + "opencode-windows-arm64": ["opencode-windows-arm64@1.19.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4rY75jyh4A/q0eIWLzo3g4yigWceRLdFUpb49hHeGFJgEXjNIWWT0P+OUzYcA3EQeJtTyRcIlLOjFqr+AeMi1Q=="], + + "opencode-windows-x64": ["opencode-windows-x64@1.19.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Rr5k+esRBsEBGCjqjVwvFpybQMmDO18Hu5vlBWGj4m0aMMoeR/m+qcW/tJGIKhQXy7Mu31izTn60LXjDMwfFAg=="], + + "opencode-windows-x64-baseline": ["opencode-windows-x64-baseline@1.19.4", "", { "os": "win32", "cpu": "x64" }, "sha512-7CPsIyrI6Jzef8e0YlCHQ3HgmNADSTZWgzfS2e/P8LScMG7D5xVFro/2pHkE+VCjZ42NGs1ZdMnSdfsKpx6h8g=="], + + "p-map": ["p-map@7.1.1", "", {}, "sha512-6Lh3jQPLthYujSrlw6ccWXBGZTkoTCz27Hd92g79LH/kjAbgoEu0t442cCCNbegPSJsA0lwDX8z0bcKtkpA32g=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "pacote": ["pacote@21.5.1", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^13.0.0", "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" } }, "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg=="], + + "parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@7.1.7", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g=="], + + "powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="], + + "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], + + "proggy": ["proggy@4.0.0", "", {}, "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ=="], + + "promise-all-reject-late": ["promise-all-reject-late@1.0.1", "", {}, "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw=="], + + "promise-call-limit": ["promise-call-limit@3.0.2", "", {}, "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw=="], + + "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], + + "read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="], + + "redis": ["redis@6.3.0", "", { "dependencies": { "@redis/bloom": "6.3.0", "@redis/client": "6.3.0", "@redis/json": "6.3.0", "@redis/search": "6.3.0", "@redis/time-series": "6.3.0" } }, "sha512-XFQbPie1lGpKeUZ8ySYY43yLnQy/iIQtHNvIr/1XdaABpb96rUSdm3Mfl+98FEVbv9ZJIzINMno5s0Nh8E5LEw=="], + + "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sigstore": ["sigstore@4.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/sign": "^4.1.1", "@sigstore/tuf": "^4.0.2", "@sigstore/verify": "^3.1.1" } }, "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "socket.io-client": ["socket.io-client@4.8.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-7wjKAvVXYOJsbmUgkWbnhPD4bFpkqrUR0++XOjpGFmqDqsqV1CI93VTraeExLxpjxEqrWgFoSRZu1ihjLWfkAA=="], + + "socket.io-parser": ["socket.io-parser@4.2.7", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg=="], + + "socks": ["socks@2.8.10", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.24", "", {}, "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], + + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="], + + "tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="], + + "treeverse": ["treeverse@3.0.0", "", {}, "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ=="], + + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tuf-js": ["tuf-js@4.1.0", "", { "dependencies": { "@tufjs/models": "4.1.0", "debug": "^4.4.3", "make-fetch-happen": "^15.0.1" } }, "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@8.12.3", "", {}, "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw=="], + + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@14.0.3", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw=="], + + "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], + + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], + + "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="], + + "ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="], + + "wsl-utils": ["wsl-utils@1.0.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.2.0" } }, "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ=="], + + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], + + "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + + "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@modelcontextprotocol/client/jose": ["jose@6.2.13", "", {}, "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ=="], + + "@modelcontextprotocol/client/zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], + + "@modelcontextprotocol/core/zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], + + "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + + "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + + "@opencode-ai/plugin/effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="], + + "@opencode/simulation/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], + + "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "engine.io-client/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], + + "gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + + "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "node-gyp/undici": ["undici@6.29.1", "", {}, "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA=="], + + "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "tinyglobby/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="], + + "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "rimraf/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], + + "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + } +} diff --git a/adapters/opencode-sdk/package.json b/adapters/opencode-sdk/package.json new file mode 100644 index 00000000..375642ba --- /dev/null +++ b/adapters/opencode-sdk/package.json @@ -0,0 +1,28 @@ +{ + "name": "scopeweaver-opencode-sdk", + "private": true, + "version": "0.1.0", + "type": "module", + "engines": { + "bun": ">=1.3.14 <2" + }, + "scripts": { + "start": "bun --no-env-file src/server.mjs", + "test": "bun test --timeout 30000", + "check": "node --check src/server.mjs && node --check src/bridge.mjs && node --check src/worker.mjs && node --check src/native.mjs && node --check src/protocol.mjs && node --check src/legacy.mjs && node --check src/legacy-plugin.mjs && node --check src/native-cli.mjs && node --check src/native-plugin.mjs" + }, + "dependencies": { + "@opencode/ai": "2.1.4", + "@opencode/client": "2.1.4", + "@opencode/core": "2.1.4", + "@opencode/plugin": "2.1.4", + "@opencode/sdk": "2.1.4", + "@opencode/util": "2.1.4", + "@opencode-ai/sdk": "1.19.4", + "@opencode-ai/plugin": "1.19.4", + "opencode-ai": "1.19.4", + "effect": "4.0.1", + "jsonc-parser": "3.3.1", + "@opencode/cli": "2.1.4" + } +} diff --git a/adapters/opencode-sdk/src/bridge.mjs b/adapters/opencode-sdk/src/bridge.mjs new file mode 100644 index 00000000..fb789d3d --- /dev/null +++ b/adapters/opencode-sdk/src/bridge.mjs @@ -0,0 +1,137 @@ +import { spawn } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { timingSafeEqual } from "node:crypto"; +import { StringDecoder } from "node:string_decoder"; +import { readFileSync } from "node:fs"; +import { BridgeError, fail, publicError, safeJSON, parseRequest, validateProviderConfig, serializeComplete } from "./protocol.mjs"; + +// Each worker owns its SDK state, storage, environment, and one model request. +export const MAX_REQUEST_BYTES=16*1024*1024; +export function workerEnvironment(root,source=process.env) { + const env={PATH:source.PATH??"/usr/bin:/bin",TMPDIR:root,OPENCODE_TEST_HOME:join(root,"home"),XDG_CONFIG_HOME:join(root,"xdg-config"),XDG_DATA_HOME:join(root,"xdg-data"),XDG_CACHE_HOME:join(root,"xdg-cache"),XDG_STATE_HOME:join(root,"xdg-state")}; + for(const key of ["SystemRoot","WINDIR","LANG","LC_ALL","TZ"])if(source[key])env[key]=source[key]; + return env; +} +export async function executeWorker(input,{signal,onEvent=()=>{},timeout=0}={}) { + const root=await mkdtemp(join(tmpdir(),"scopeweaver-opencode-worker-")); + const env=workerEnvironment(root); + let child,timer,killTimer,canceled=false,timedOut=false; + try { + return await new Promise((resolve,reject)=>{ + const grouped=process.platform!=="win32"; + child=spawn(process.execPath,["--no-env-file",fileURLToPath(new URL("./worker.mjs",import.meta.url))],{cwd:root,env,detached:grouped,stdio:["pipe","pipe","pipe"]}); + let buffer="",size=0,result,error,pendingStop;const decoder=new StringDecoder("utf8"); + const kill=signal=>{try{if(grouped&&child.pid)process.kill(-child.pid,signal);else child.kill(signal)}catch{}}; + const stop=()=>{kill("SIGTERM");killTimer??=setTimeout(()=>kill("SIGKILL"),1500)}; + const abort=()=>{canceled=true;stop()};signal?.addEventListener("abort",abort,{once:true}); + if(timeout>0)timer=setTimeout(()=>{timedOut=true;stop()},timeout); + if(signal?.aborted)abort(); + child.stderr.on("data",()=>{}); // Provider errors may contain credentials; never forward stderr. + child.stdout.on("data",chunk=>{ + size+=chunk.length;if(size>20000000){error=new BridgeError("protocol_error",502);stop();return} + buffer+=decoder.write(chunk); + let index;while((index=buffer.indexOf("\n"))>=0){ + const line=buffer.slice(0,index);buffer=buffer.slice(index+1);if(!line)continue; + try { + const value=safeJSON(line,"protocol_error"); + if(value.kind==="event"){ + if(value.event.type==="message_stop"){if(pendingStop)throw new BridgeError("protocol_error",502);pendingStop=value.event} + else onEvent(value.event); + } + else if(value.kind==="result")result=value.result; + else if(value.kind==="models")result=value.models; + else if(value.kind==="error")error=new BridgeError(value.code,({unknown_model:400,unsupported_input:422,invalid_request:400,configuration_error:400,canceled:499,timeout:504,quota_exhausted:402,context_length_exceeded:400,free_tier_restricted:403})[value.code]??502); + else throw new BridgeError("protocol_error",502); + }catch(e){error=e;stop()} + } + }); + child.on("error",()=>{error=new BridgeError("sdk_failure",502)}); + child.on("close",code=>{ + signal?.removeEventListener("abort",abort);clearTimeout(timer);clearTimeout(killTimer); + kill("SIGKILL"); // An SDK-owned subprocess must not outlive its request worker. + if(timedOut)reject(new BridgeError("timeout",504)); + else if(canceled||signal?.aborted)reject(new BridgeError("canceled",499)); + else if(error)reject(error); + else if(code!==0||result===undefined||(buffer+decoder.end()).trim()||(input.mode!=="models"&&!pendingStop))reject(new BridgeError("sdk_failure",502)); + else {try{if(pendingStop)onEvent(pendingStop);resolve(result)}catch{reject(new BridgeError("canceled",499))}} + }); + child.stdin.on("error",()=>{}); + child.stdin.end(JSON.stringify({...input,root})); + }); + } finally {clearTimeout(timer);clearTimeout(killTimer);await rm(root,{recursive:true,force:true})} +} + +const encoder=new TextEncoder(); +const nativeVersionMatches=["@opencode/cli/package.json","@opencode/sdk","@opencode/client","@opencode/plugin","@opencode/ai","@opencode/core/config","@opencode/util/global"].every(id=>{ + try{ + let path=dirname(fileURLToPath(import.meta.resolve(id))); + const name=id.split("/").slice(0,2).join("/"); + while(path!==dirname(path)){try{const p=JSON.parse(readFileSync(join(path,"package.json"),"utf8"));if(p.name===name)return p.version==="2.1.4"}catch{}path=dirname(path)} + return false; + }catch{return false} +}); +const legacyVersionMatches=["@opencode-ai/sdk/v2","opencode-ai/package.json"].every(id=>{ + try{ + let path=dirname(fileURLToPath(import.meta.resolve(id))); + const name=id.startsWith("@")?id.split("/").slice(0,2).join("/"):id.split("/")[0]; + while(path!==dirname(path)){try{const p=JSON.parse(readFileSync(join(path,"package.json"),"utf8"));if(p.name===name)return p.version==="1.19.4"}catch{}path=dirname(path)} + return false; + }catch{return false} +}); +const json=(value,status=200)=>new Response(JSON.stringify(value),{status,headers:{"content-type":"application/json","cache-control":"no-store"}}); +async function readBody(req) { + if(Number(req.headers.get("content-length"))>MAX_REQUEST_BYTES)fail("body_too_large",413); + const reader=req.body?.getReader();if(!reader)fail("invalid_request"); + const chunks=[];let size=0; + try{while(true){const {done,value}=await reader.read();if(done)break;size+=value.byteLength;if(size>MAX_REQUEST_BYTES){await reader.cancel();fail("body_too_large",413)}chunks.push(value)}}finally{reader.releaseLock()} + return new TextDecoder("utf-8",{fatal:true}).decode(Buffer.concat(chunks)); +} +export function createBridge({token,providerConfig,timeout=0,runtime="native",worker=executeWorker}={}) { + if(typeof token!=="string"||token.length<32||/[\s\r\n]/.test(token))throw new Error("SCOPEWEAVER_OPENCODE_TOKEN must contain at least 32 non-whitespace characters"); + if(!Number.isSafeInteger(timeout)||timeout<0||timeout>86400000)throw new Error("Bridge timeout must be 0..86400000 milliseconds"); + providerConfig=validateProviderConfig(providerConfig); + const active=new Map();let closed=false; + const authorize=req=>{ + const u=new URL(req.url); + if(req.headers.has("origin")||!["127.0.0.1","localhost","[::1]"].includes(u.hostname)||u.username||u.password||u.search||u.hash)fail("forbidden",403); + const supplied=req.headers.get("authorization")??"",expected=`Bearer ${token}`; + if(Buffer.byteLength(supplied)!==Buffer.byteLength(expected)||!timingSafeEqual(Buffer.from(supplied),Buffer.from(expected)))fail("unauthorized",401); + if(!((runtime==="native"&&nativeVersionMatches&&process.platform!=="win32")||(runtime==="legacy"&&legacyVersionMatches&&process.platform!=="win32")))fail("unsupported_runtime",400); + }; + const controllerFor=req=>{ + const controller=new AbortController();const abort=()=>controller.abort();req.signal.addEventListener("abort",abort,{once:true});if(req.signal.aborted)abort(); + return {controller,dispose:()=>req.signal.removeEventListener("abort",abort)}; + }; + return { + async fetch(req){ + try { + authorize(req);if(closed)fail("canceled",503); + const path=new URL(req.url).pathname; + if(req.method==="GET"&&path==="/v1/capabilities")return json({protocol:"scopeweaver-opencode-sdk",version:1,sdk_version:runtime==="native"?"2.1.4":"1.19.4",runtime,tool_execution:"capture-only",streaming:true}); + if(req.method==="DELETE"&&/^\/v1\/requests\/[A-Za-z0-9_-]{1,128}$/.test(path)){active.get(path.slice("/v1/requests/".length))?.abort();return json({aborted:true})} + if(req.method==="GET"&&path==="/v1/models"){ + const {controller,dispose}=controllerFor(req);const key=Symbol();active.set(key,controller); + try{return json({models:await worker({mode:"models",runtime,providerConfig},{signal:controller.signal,timeout})})}finally{active.delete(key);dispose()} + } + if(req.method!=="POST"||!["/v1/complete","/v1/stream"].includes(path))fail("not_found",404); + if(!req.headers.get("content-type")?.toLowerCase().startsWith("application/json"))fail("invalid_request",415); + const {request,rawHistory}=parseRequest(await readBody(req)); + if(active.has(request.request_id))fail("duplicate_request",409); + const {controller,dispose}=controllerFor(req);active.set(request.request_id,controller); + const cleanup=()=>{active.delete(request.request_id);dispose()}; + if(path==="/v1/complete"){ + try{const result=await worker({mode:"complete",runtime,request,rawHistory,providerConfig},{signal:controller.signal,timeout});return new Response(serializeComplete(result),{headers:{"content-type":"application/json","cache-control":"no-store"}})}finally{cleanup()} + } + const stream=new ReadableStream({start(output){ + void worker({mode:"complete",runtime,request,rawHistory,providerConfig},{signal:controller.signal,timeout,onEvent:event=>output.enqueue(encoder.encode(JSON.stringify(event)+"\n"))}) + .then(()=>{output.close()},error=>{try{output.enqueue(encoder.encode(JSON.stringify({type:"error",...publicError(error)})+"\n"));output.close()}catch{}}).finally(cleanup); + },cancel(){controller.abort()}}); + return new Response(stream,{headers:{"content-type":"application/x-ndjson","cache-control":"no-store"}}); + }catch(error){return json({error:publicError(error)},error instanceof BridgeError?error.status:400)} + }, + async close(){closed=true;for(const c of active.values())c.abort();while(active.size)await new Promise(resolve=>setTimeout(resolve,20));}, + }; +} diff --git a/adapters/opencode-sdk/src/legacy-plugin.mjs b/adapters/opencode-sdk/src/legacy-plugin.mjs new file mode 100644 index 00000000..275ce25f --- /dev/null +++ b/adapters/opencode-sdk/src/legacy-plugin.mjs @@ -0,0 +1,79 @@ +// Loaded only by the owned, isolated CLI 1.19.4 process. No host tools execute here. +import { readFileSync } from "node:fs"; + +export default async (_input, options) => { + const state = JSON.parse(readFileSync(options.state, "utf8")); + const attempts = new Map(); + let network = 0, quota = false, contextOverflow = false, freeTier = false, redirected = false; + const blocked = () => redirected ? "SCOPEWEAVER_LEGACY_REDIRECT" : freeTier ? "SCOPEWEAVER_LEGACY_FREE_TIER" : contextOverflow ? "SCOPEWEAVER_LEGACY_CONTEXT_LENGTH" : quota ? "SCOPEWEAVER_LEGACY_QUOTA" : "SCOPEWEAVER_LEGACY_SECOND_REQUEST"; + return { + config: async config => { + config.provider ??= {}; + config.provider[state.provider] ??= {}; + config.provider[state.provider].options ??= {}; + // Public provider options support custom fetch. It gates actual network + // attempts too, including AI SDK retries, and refuses HTTP redirects. + config.provider[state.provider].options.fetch = async (input, init) => { + if (++network !== 1) throw Error(blocked()); + const response = await fetch(input, { ...init, redirect: "error" }).catch(error => { + // Preserve the first redirect failure if the CLI retries either gate. + redirected = error?.code === "UnexpectedRedirect"; + throw error; + }); + if ([400, 413, 402, 429, 403].includes(response.status)) { + const reader = response.clone().body?.getReader(); + if (reader) { + try { + const decoder = new TextDecoder(); let text = ""; + while (text.length < 100000) { const chunk = await reader.read(); if (chunk.done) break; text += decoder.decode(chunk.value, { stream: true }); } + if (text.length < 100000) { + const body = JSON.parse(text); + quota = [402, 429].includes(response.status) && ["insufficient_quota", "billing_hard_limit_reached", "billing_not_active", "credit_balance_exhausted", "quota_exceeded"].includes(body?.error?.code); + contextOverflow = [400, 413].includes(response.status) && [body?.error?.code, body?.error?.type].some(code => ["context_length_exceeded", "model_context_window_exceeded", "context_window_exceeded", "prompt_too_long"].includes(code)); + freeTier = response.status === 403 && [body?.error?.code, body?.error?.type, body?.error?.name].includes("FreeTierError"); + } + } catch {} finally { void reader.cancel().catch(() => {}); } + } + } + // The CLI otherwise normalizes context errors and can discard the + // structured HTTP evidence. Preserve only our proven typed marker. + if (contextOverflow || freeTier) throw Error(blocked()); + return response; + }; + }, + "tool.execute.before": async input => { + if (!state.allowed.includes(input.tool)) throw Error("SCOPEWEAVER_LEGACY_NATIVE_TOOL"); + }, + "chat.params": async (input, output) => { + const count = (attempts.get(input.sessionID) ?? 0) + 1; + attempts.set(input.sessionID, count); + if (count !== 1) throw Error(blocked()); + output.maxOutputTokens = state.request.max_tokens; + if (state.request.temperature !== undefined) output.temperature = state.request.temperature; + output.options = { + ...output.options, + ...(state.request.stop !== undefined ? { stop: state.request.stop } : {}), + ...(state.request.thinking || state.request.thinking_type + ? { thinking: { type: state.request.thinking || state.request.thinking_type } } : {}), + ...(state.request.reasoning_effort ? { reasoningEffort: state.request.reasoning_effort } : {}), + }; + }, + "chat.headers": async (_input, output) => { + // OpenCode owns its internal session identity; upstream affinity remains host-owned. + if (["opencode", "opencode-go"].includes(state.provider)) + output.headers["x-opencode-session"] = state.request.session_id ?? state.request.request_id; + }, + "experimental.chat.system.transform": async (_input, output) => { + output.system.splice(0, output.system.length, ...state.request.system); + }, + "experimental.chat.messages.transform": async (_input, output) => { + const sessionID = output.messages[0]?.info.sessionID; + if (!sessionID) throw Error("SCOPEWEAVER_LEGACY_HISTORY"); + const history = state.history.map(message => ({ + info: { ...message.info, sessionID }, + parts: message.parts.map(part => ({ ...part, sessionID })), + })); + output.messages.splice(0, output.messages.length, ...history); + }, + }; +}; diff --git a/adapters/opencode-sdk/src/legacy.mjs b/adapters/opencode-sdk/src/legacy.mjs new file mode 100644 index 00000000..467e7dc8 --- /dev/null +++ b/adapters/opencode-sdk/src/legacy.mjs @@ -0,0 +1,365 @@ +import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"; +import { createRequire } from "node:module"; +import { createServer } from "node:http"; +import { spawn } from "node:child_process"; +import { mkdir, writeFile, readFile, access, cp, lstat } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { randomUUID } from "node:crypto"; +import { fileURLToPath } from "node:url"; +import { BridgeError, fail, safeJSON } from "./protocol.mjs"; + +const require = createRequire(import.meta.url); +const gateMarker = "SCOPEWEAVER_LEGACY_SECOND_REQUEST"; +const canceled = () => new BridgeError("canceled", 499); +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); +function nativeError(error) { + const data = error?.data; + if (data?.message === "SCOPEWEAVER_LEGACY_QUOTA") return new BridgeError("quota_exhausted", 402); + if (data?.message === "SCOPEWEAVER_LEGACY_CONTEXT_LENGTH") return new BridgeError("context_length_exceeded", 400); + if (data?.message === "SCOPEWEAVER_LEGACY_FREE_TIER") return new BridgeError("free_tier_restricted", 403); + if (error?.name === "APIError" && data?.statusCode === 403 && typeof data.responseBody === "string" && data.responseBody.length < 100000) { + try { + const body = JSON.parse(data.responseBody); + if ([body?.error?.code, body?.error?.type, body?.error?.name].includes("FreeTierError")) return new BridgeError("free_tier_restricted", 403); + } catch {} + } + if (error?.name === "APIError" && [400, 413].includes(data?.statusCode) && typeof data.responseBody === "string" && data.responseBody.length < 100000) { + try { + const body = JSON.parse(data.responseBody), codes = [body?.error?.code, body?.error?.type]; + if (codes.some(code => ["context_length_exceeded", "model_context_window_exceeded", "context_window_exceeded", "prompt_too_long"].includes(code))) + return new BridgeError("context_length_exceeded", 400); + } catch {} + } + if (error?.name === "APIError" && [402, 429].includes(data?.statusCode) && typeof data.responseBody === "string" && data.responseBody.length < 100000) { + try { + const body = JSON.parse(data.responseBody); + if (["insufficient_quota", "billing_hard_limit_reached", "billing_not_active", "credit_balance_exhausted", "quota_exceeded"].includes(body?.error?.code)) + return new BridgeError("quota_exhausted", 402); + } catch {} + } + return new BridgeError(data?.message === gateMarker ? "second_model_request" : "sdk_failure", 502); +} + +async function binary() { + const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[process.platform]; + if (!platform || !["arm64", "x64"].includes(process.arch)) fail("unsupported_runtime"); + // opencode-ai's postinstall is intentionally unnecessary (--ignore-scripts supported). + let musl = false; + if (process.platform === "linux" && !process.report?.getReport?.()?.header?.glibcVersionRuntime) { + try { await access(`/lib/ld-musl-${process.arch === "arm64" ? "aarch64" : "x86_64"}.so.1`); musl = true; } catch {} + } + const base = `opencode-${platform}-${process.arch}`, suffix = musl ? "-musl" : ""; + const names = process.arch === "x64" ? [`${base}-baseline${suffix}`, `${base}${suffix}`] : [`${base}${suffix}`]; + let path; + for (const name of names) { + try { path = require.resolve(`${name}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`); break; } catch {} + } + if (!path) fail("unsupported_runtime"); + if (JSON.parse(await readFile(resolve(dirname(path), "../package.json"), "utf8")).version !== "1.19.4") fail("unsupported_runtime"); + await access(path); + return path; +} + +function aliases(request) { + const forward = new Map(), reverse = new Map(); + const names = [...request.tools.map(tool => tool.name), ...request.messages.flatMap(message => message.content.filter(b => b.type === "tool_use").map(b => b.name))]; + for (const name of names) if (!forward.has(name)) { + let local = /^[A-Za-z_][A-Za-z0-9_-]{0,55}$/.test(name) ? name : `host_${forward.size}`; + let suffix = 0; + while (reverse.has(`capture_${local}`)) local = `host_${forward.size}_${++suffix}`; + const alias = `capture_${local}`; + forward.set(name, alias); reverse.set(alias, name); + } + return { forward, reverse }; +} + +function history(request, map, rawHistory) { + // CLI 1.x carries structural JSON. Normal whitespace and string escapes + // (including Go's HTML escaping) preserve values; unsafe numbers do not. + // Native2 remains the lossless path for raw bytes and 64-bit IDs. + for (const raw of Object.values(rawHistory)) { + safeJSON(raw, "unsupported_input"); + } + const results = new Map(); + for (const message of request.messages) for (const block of message.content) + if (block.type === "tool_result") results.set(block.tool_use_id, block); + const [providerID, ...modelParts] = request.model.split("/"); + const modelID = modelParts.join("/"); + return request.messages.map((message, index) => { + const id = `msg_scopeweaver_${index}`, parts = []; + for (const block of message.content) { + const common = { id: `prt_scopeweaver_${index}_${parts.length}`, messageID: id }; + if (block.type === "text") parts.push({ ...common, type: "text", text: block.text }); + else if (block.type === "thinking") { + // The pinned OpenAI-compatible converter preserves unsigned text only; + // opaque provider signatures would be silently discarded, even if the + // current request disables thinking. Reject before creating a runtime. + if (block.signature !== undefined && block.signature !== "") fail("unsupported_input"); + if (request.thinking !== "disabled") parts.push({ ...common, type: "reasoning", text: block.thinking, + time: { start: 0, end: 0 } }); + } else if (block.type === "tool_use") { + const result = results.get(block.id); if (!result) fail("unsupported_input"); + const raw = rawHistory[block.id] ?? JSON.stringify(block.input); + const input = safeJSON(raw, "unsupported_input"); + const text = result.content.map(part => part.text).join("\n"); + parts.push({ ...common, type: "tool", callID: block.id, tool: map.forward.get(block.name), + state: result.is_error ? { status: "error", input, error: text, time: { start: 0, end: 0 } } + : { status: "completed", input, output: text, title: "", metadata: {}, time: { start: 0, end: 0 } } }); + } else if (block.type !== "tool_result") fail("unsupported_input"); + } + const info = message.role === "assistant" + ? { id, role: "assistant", parentID: `msg_scopeweaver_${Math.max(0, index - 1)}`, modelID, providerID, + agent: "capture", mode: "capture", path: { cwd: "/", root: "/" }, cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: 0, completed: 0 }, finish: "stop" } + : { id, role: "user", time: { created: 0 }, agent: "capture", model: { providerID, modelID } }; + return { info, parts }; + }).filter(message => message.parts.length); +} + +function providers(config) { + if (!config) return {}; + const out = {}; + for (const [id, provider] of Object.entries(config.providers)) { + // The trusted common configuration deliberately has a narrow legacy mapping. + if (provider.package !== "@opencode/ai/providers/openai-compatible") fail("configuration_error"); + const models = {}; + for (const [modelID, model] of Object.entries(provider.models)) { + if (model.compatibility?.maxTokensField && model.compatibility.maxTokensField !== "max_tokens") fail("unsupported_input"); + if (model.compatibility?.reasoningField || Object.keys(model.capabilities ?? {}).some(key => key !== "tools")) fail("unsupported_input"); + models[modelID] = { id: modelID, name: model.name ?? modelID, tool_call: model.capabilities?.tools ?? true, temperature: true, + limit: { context: model.limit?.context ?? 200000, output: model.limit?.output ?? 32768 } }; + } + out[id] = { name: provider.name ?? id, npm: "@ai-sdk/openai-compatible", options: provider.settings, models }; + } + return out; +} + +async function start({ root, providerConfig, request, map, rawHistory = {}, signal }) { + if (signal?.aborted) throw canceled(); + const runtime = `${root}/legacy`; + for (const dir of ["home", "config", "data", "state", "cache", "work"]) await mkdir(`${runtime}/${dir}`, { recursive: true }); + // The CLI normally bootstraps its plugin package on startup. Give it a real, + // private package tree and matching lock; never a writable link to our tree. + const pluginRoot = resolve(dirname(fileURLToPath(import.meta.resolve("@opencode-ai/plugin"))), ".."); + if (JSON.parse(await readFile(`${pluginRoot}/package.json`, "utf8")).version !== "1.19.4") fail("unsupported_runtime"); + for (const dir of [`${runtime}/config`, `${runtime}/config/opencode`]) { + await mkdir(dir, { recursive: true }); + const dependencies = { "@opencode-ai/plugin": "1.19.4" }; + await writeFile(`${dir}/package.json`, JSON.stringify({ dependencies })); + await writeFile(`${dir}/package-lock.json`, JSON.stringify({ lockfileVersion: 3, packages: { "": { dependencies } } })); + try { if ((await lstat(`${dir}/node_modules`)).isSymbolicLink()) fail("configuration_error"); } + catch (error) { if (error.code !== "ENOENT") throw error; } + await mkdir(`${dir}/node_modules/@opencode-ai`, { recursive: true }); + await cp(pluginRoot, `${dir}/node_modules/@opencode-ai/plugin`, { recursive: true, + filter: source => !source.slice(pluginRoot.length).split(/[\\/]/).includes("node_modules") }); + } + const configured = providers(providerConfig), captures = new Map(); + let captureServer, processHandle, stopped = false, captureListening = false, captureClosing; + const stopNow = () => { + if (processHandle && processHandle.exitCode === null && processHandle.signalCode === null) processHandle.kill("SIGTERM"); + if (captureServer && captureListening && !captureClosing) { + captureServer.closeAllConnections(); captureClosing = new Promise(resolve => captureServer.close(() => resolve())); + } + }; + signal?.addEventListener("abort", stopNow, { once: true }); + const close = async () => { + if (stopped) return; stopped = true; + signal?.removeEventListener("abort", stopNow); + stopNow(); + const exited = () => processHandle.exitCode !== null || processHandle.signalCode !== null; + if (processHandle && !exited()) { + processHandle.kill("SIGTERM"); + await Promise.race([new Promise(resolve => processHandle.once("exit", resolve)), sleep(1500)]); + if (!exited()) { + processHandle.kill("SIGKILL"); + await Promise.race([new Promise(resolve => processHandle.once("exit", resolve)), sleep(1500)]); + if (!exited()) fail("sdk_failure", 502); + } + } + if (captureClosing) await captureClosing; + }; + try { + const cfg = { autoupdate: false, share: "disabled", snapshot: false, compaction: { auto: false }, plugin: [], + lsp: false, formatter: false, permission: { "*": "deny" }, tools: { "*": false }, + agent: { capture: { mode: "primary", permission: { "*": "deny" }, tools: { "*": false } }, title: { disable: true }, summary: { disable: true }, compaction: { disable: true } }, + default_agent: "capture", provider: configured }; + if (request) { + const separator = request.model.indexOf("/"), provider = request.model.slice(0, separator), model = request.model.slice(separator + 1); + cfg.model = request.model; cfg.small_model = request.model; + if (providerConfig && !configured[provider]?.models[model]) fail("unknown_model"); + if (request.tools.length && configured[provider]?.models[model]?.tool_call === false) fail("unsupported_input"); + const max = configured[provider]?.models[model]?.limit.output; + if (max && request.max_tokens > max) request = { ...request, max_tokens: max }; + if (configured[provider]) configured[provider].models[model].limit.output = request.max_tokens; + const statePath = `${runtime}/state.json`; + await writeFile(statePath, JSON.stringify({ request, provider, allowed: request.tools.map(tool => map.forward.get(tool.name)), history: history(request, map, rawHistory) }), { mode: 0o600 }); + cfg.plugin = [[new URL("./legacy-plugin.mjs", import.meta.url).href, { state: statePath }]]; + const token = randomUUID(), schemas = request.tools.map(tool => ({ name: map.forward.get(tool.name).slice("capture_".length), description: `ScopeWeaver host tool ${tool.name}. Invoke this advertised function when host instructions refer to ${tool.name}.\n${tool.description}`, inputSchema: tool.input_schema })); + captureServer = createServer(async (req, res) => { + if (req.headers.authorization !== `Bearer ${token}`) { res.writeHead(401); res.end(); return; } + if (req.method !== "POST") { res.writeHead(req.method === "DELETE" ? 200 : 405); res.end(); return; } + try { + let text = ""; for await (const chunk of req) { text += chunk; if (text.length > 2000000) throw Error(); } + const call = safeJSON(text, "protocol_error"); + if (call.id === undefined) { res.writeHead(202); res.end(); return; } + let result; + if (call.method === "initialize") result = { protocolVersion: call.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "scopeweaver-capture", version: "1" } }; + else if (call.method === "tools/list") result = { tools: schemas }; + else if (call.method === "tools/call") { + if (!schemas.some(schema => schema.name === call.params.name)) throw Error(); + const key = `capture_${call.params.name}`, values = captures.get(key) ?? []; + values.push(JSON.stringify(call.params.arguments)); captures.set(key, values); + result = { content: [{ type: "text", text: "deferred_to_host" }] }; + } else if (call.method === "ping") result = {}; + else { res.writeHead(400); res.end(); return; } + res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", id: call.id, result })); + } catch { res.writeHead(400); res.end(); } + }); + await new Promise(resolve => captureServer.listen(0, "127.0.0.1", resolve)); + captureListening = true; + if (signal?.aborted) throw canceled(); + cfg.mcp = { capture: { type: "remote", url: `http://127.0.0.1:${captureServer.address().port}/mcp`, headers: { Authorization: `Bearer ${token}` }, oauth: false, enabled: true, timeout: 10000 } }; + for (const tool of request.tools) { + const name = map.forward.get(tool.name); + cfg.permission[name] = "allow"; cfg.tools[name] = true; + cfg.agent.capture.permission[name] = "allow"; cfg.agent.capture.tools[name] = true; + } + } + await writeFile(`${runtime}/opencode.json`, JSON.stringify(cfg), { mode: 0o600 }); + const password = randomUUID(); + const env = { PATH: `${process.execPath.slice(0, process.execPath.lastIndexOf("/"))}:/usr/bin:/bin`, HOME: `${runtime}/home`, + XDG_CONFIG_HOME: `${runtime}/config`, XDG_DATA_HOME: `${runtime}/data`, XDG_STATE_HOME: `${runtime}/state`, XDG_CACHE_HOME: `${runtime}/cache`, + OPENCODE_CONFIG: `${runtime}/opencode.json`, OPENCODE_CONFIG_DIR: `${runtime}/config`, OPENCODE_DISABLE_DEFAULT_PLUGINS: "true", + OPENCODE_DISABLE_AUTOUPDATE: "true", OPENCODE_DISABLE_MODELS_FETCH: "true", OPENCODE_DISABLE_LSP_DOWNLOAD: "true", OPENCODE_DISABLE_PROJECT_CONFIG: "true", OPENCODE_SERVER_PASSWORD: password, CI: "1" }; + // CLI treats --port 0 as its default port; reserve then release a real ephemeral port. + const reservation = createServer(); await new Promise(resolve => reservation.listen(0, "127.0.0.1", resolve)); + const port = reservation.address().port; await new Promise(resolve => reservation.close(resolve)); + const executable = await binary(); + if (signal?.aborted) throw canceled(); + processHandle = spawn(executable, ["serve", "--hostname", "127.0.0.1", "--port", String(port)], { cwd: `${runtime}/work`, env, stdio: ["ignore", "pipe", "pipe"] }); + const url = await new Promise((resolve, reject) => { + let text = ""; + const timer = setTimeout(() => reject(new BridgeError("sdk_failure", 502)), 20000); + const abort = () => { clearTimeout(timer); reject(canceled()); }; + signal?.addEventListener("abort", abort, { once: true }); + const data = chunk => { + text = (text + chunk).slice(-4000); + if (text.includes(`http://127.0.0.1:${port}`)) { clearTimeout(timer); signal?.removeEventListener("abort", abort); resolve(`http://127.0.0.1:${port}`); } + }; + processHandle.stdout.on("data", data); processHandle.stderr.on("data", data); + processHandle.once("error", () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); reject(new BridgeError("unsupported_runtime")); }); + processHandle.once("exit", () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); reject(new BridgeError("sdk_failure", 502)); }); + }); + return { client: createOpencodeClient({ baseUrl: url, directory: `${runtime}/work`, headers: { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` }, throwOnError: true }), directory: `${runtime}/work`, close, stopNow, captures, request }; + } catch (error) { await close(); throw error; } +} + +export async function discoverLegacyModels({ root, providerConfig }) { + const runtime = await start({ root, providerConfig }); + try { + const data = (await runtime.client.provider.list()).data; + let publicIDs; + if (!providerConfig) { + const response = await fetch("https://opencode.ai/inference/v1/models", { redirect: "error", signal: AbortSignal.timeout(10000) }); + if (!response.ok) fail("sdk_failure", 502); + const catalog = safeJSON(await response.text(), "sdk_failure"); publicIDs = new Set(catalog.data.map(model => model.id)); + } + const models = data.all.flatMap(provider => Object.values(provider.models).filter(model => providerConfig + ? Object.hasOwn(providerConfig.providers, provider.id) && Object.hasOwn(providerConfig.providers[provider.id].models, model.id) + : provider.id === "opencode" && publicIDs.has(model.id) && model.cost?.input === 0 && model.cost?.output === 0) + .map(model => ({ id: `${provider.id}/${model.id}`, name: model.name ?? model.id }))); + if (!models.length) fail("unknown_model"); + return models.sort((a, b) => a.id.localeCompare(b.id)); + } finally { await runtime.close(); } +} + +export async function completeLegacy(request, { root, providerConfig, rawHistory = {}, signal, emit = () => {} }) { + const map = aliases(request); + history(request, map, rawHistory); // all representability failures precede SDK inference + if (!providerConfig && !(await discoverLegacyModels({ root: `${root}/catalog` })).some(model => model.id === request.model)) fail("unknown_model"); + const runtime = await start({ root, providerConfig, request, map, rawHistory, signal }); + const { client, captures, directory } = runtime; + const abortEvents = new AbortController(), blocks = new Map(), content = [], calls = new Map(); + let session, consumer, error, firstUsage, finish, ended = false, bytes = 0, wake; + const notify = () => { wake?.(); wake = undefined; }; + const abort = () => { runtime.stopNow(); error = canceled(); notify(); }; + signal?.addEventListener("abort", abort, { once: true }); + const cleanupSession = async () => { + if (!session) return; + await Promise.race([(async () => { + await client.session.abort({ sessionID: session.id, directory }); + await client.session.delete({ sessionID: session.id, directory }); + })(), sleep(1500)]); + session = undefined; + }; + try { + if (signal?.aborted) throw canceled(); + const sub = await client.event.subscribe({ directory }, { signal: abortEvents.signal }); + consumer = (async () => { + try { for await (const event of sub.stream) { + if (event.properties?.sessionID !== session?.id && event.properties?.part?.sessionID !== session?.id) continue; + const part = event.properties?.part; + if (event.type === "message.part.updated" && part) { + if (["text", "reasoning"].includes(part.type) && !blocks.has(part.id)) { + const block = part.type === "text" ? { type: "text", text: "" } : { type: "thinking", thinking: "" }; + blocks.set(part.id, block); content.push(block); + } else if (part.type === "tool") { + if (!map.reverse.has(part.tool)) fail("protocol_error"); + if (!calls.has(part.callID)) { + if (calls.size >= 128) fail("protocol_error"); + const call = { type: "tool_use", id: `sdk_${request.request_id}_${calls.size}`, name: map.reverse.get(part.tool) }; calls.set(part.callID, { call, nativeName: part.tool }); content.push(call); + } else if (calls.get(part.callID).nativeName !== part.tool) fail("protocol_error"); + if (part.state.status === "error") fail("protocol_error"); + if (part.state.status === "completed") { + const raw = JSON.stringify(part.state.input); safeJSON(raw, "protocol_error"); + const call = calls.get(part.callID).call; + if (part.state.output !== "deferred_to_host" || (call.raw !== undefined && call.raw !== raw)) fail("protocol_error"); + if (call.raw === undefined) { + const values = captures.get(part.tool), index = values?.indexOf(raw) ?? -1; + if (index < 0 || raw.length > 2000000) fail("protocol_error"); + bytes += raw.length; if (bytes > 8000000) fail("protocol_error"); + values.splice(index, 1); call.raw = raw; + } + } + } else if (part.type === "step-finish" && !firstUsage) { + firstUsage = part.tokens; finish = part.reason; + } + } else if (event.type === "message.part.delta") { + const delta = event.properties; + if (delta.field !== "text") continue; + const block = blocks.get(delta.partID); if (!block) fail("protocol_error"); + bytes += delta.delta.length; if (bytes > 8000000) fail("protocol_error"); + block[block.type === "text" ? "text" : "thinking"] += delta.delta; + emit({ type: block.type === "text" ? "text_delta" : "thinking_delta", text: delta.delta }); + } else if (event.type === "session.error") { + if (event.properties.error?.data?.message === gateMarker && firstUsage && calls.size) ended = true; + else throw nativeError(event.properties.error); + } else if (event.type === "session.status" && event.properties.status.type === "idle" && firstUsage && !calls.size) ended = true; + notify(); + } } catch (e) { if (!abortEvents.signal.aborted) { error = e instanceof BridgeError ? e : new BridgeError("sdk_failure", 502); notify(); } } + })(); + const permissions = [{ permission: "*", pattern: "*", action: "deny" }, ...request.tools.map(tool => ({ permission: map.forward.get(tool.name), pattern: "*", action: "allow" }))]; + session = (await client.session.create({ directory, title: "ScopeWeaver capture", permission: permissions })).data; + emit({ type: "message_start" }); + const split = request.model.indexOf("/"); + await client.session.promptAsync({ sessionID: session.id, directory, model: { providerID: request.model.slice(0, split), modelID: request.model.slice(split + 1) }, agent: "capture", parts: [{ type: "text", text: "ScopeWeaver host turn" }] }); + while (!ended && !error) await new Promise(resolve => { wake = resolve; if (ended || error || signal?.aborted) notify(); }); + if (error) throw error; + if (!firstUsage || !content.length || [...calls.values()].some(value => !value.call.raw)) fail("protocol_error"); + const usage = { input_tokens: firstUsage.input, output_tokens: firstUsage.output, cache_read_tokens: firstUsage.cache?.read ?? 0, cache_write_tokens: firstUsage.cache?.write ?? 0 }; + if (!Object.values(usage).every(value => Number.isSafeInteger(value) && value >= 0)) fail("protocol_error"); + const stop_reason = calls.size ? "tool_use" : ["length", "max-tokens"].includes(finish) ? "max_tokens" : finish === "stop" ? "end_turn" : undefined; + if (!stop_reason) fail("protocol_error"); + await cleanupSession(); + abortEvents.abort(); await runtime.close(); + if (signal?.aborted) throw canceled(); + for (const { call } of calls.values()) { emit({ type: "tool_use_start", tool_id: call.id, tool_name: call.name }); emit({ type: "tool_input_delta", text: call.raw }); } + emit({ type: "message_delta", stop_reason, usage }); emit({ type: "message_stop" }); + return { message: { role: "assistant", content }, stop_reason, usage }; + } finally { + signal?.removeEventListener("abort", abort); abortEvents.abort(); + if (session) runtime.stopNow(); + try { await cleanupSession(); } catch {} + await runtime.close(); if (consumer) await Promise.race([consumer, sleep(500)]); + } +} diff --git a/adapters/opencode-sdk/src/native-cli.mjs b/adapters/opencode-sdk/src/native-cli.mjs new file mode 100644 index 00000000..c72b414b --- /dev/null +++ b/adapters/opencode-sdk/src/native-cli.mjs @@ -0,0 +1,131 @@ +import { OpenCode } from "@opencode/client"; +import { spawn } from "node:child_process"; +import { createRequire } from "node:module"; +import { mkdir, readFile, writeFile, lstat } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { userInfo } from "node:os"; +import { pathToFileURL } from "node:url"; +import { BridgeError, fail } from "./protocol.mjs"; + +const require=createRequire(import.meta.url); +export const nativeRules=[{action:"*",resource:"*",effect:"ask"},{action:"host_capture",resource:"*",effect:"allow"}]; +export function unmanagedConfigPaths(platform=process.platform,user=userInfo().username,env=process.env){ + if(platform==="darwin")return ["/Library/Application Support/opencode",`/Library/Managed Preferences/${user}/ai.opencode.managed.plist`,"/Library/Managed Preferences/ai.opencode.managed.plist"]; + return [platform==="win32"?join(env.ProgramData??"C:\\ProgramData","opencode"):"/etc/opencode"]; +} +export async function assertNoUnmanagedConfig(paths=unmanagedConfigPaths()){ + for(const path of paths){ + try{await lstat(path);fail("configuration_error");}catch(error){if(error.code!=="ENOENT")throw error instanceof BridgeError?error:new BridgeError("configuration_error");} + } +} +async function binaryPath(){ + if(!["darwin","linux"].includes(process.platform)||!["x64","arm64"].includes(process.arch))fail("unsupported_runtime"); + const platform=process.platform==="win32"?"windows":process.platform; + const base=`@opencode/cli-${platform}-${process.arch}`; + const names=process.platform==="linux"?(process.arch==="x64"?[`${base}-baseline`,`${base}-baseline-musl`,base,`${base}-musl`]:[base,`${base}-musl`]):process.arch==="x64"&&process.platform!=="win32"?[`${base}-baseline`,base]:[base]; + for(const name of names){ + try{ + const file=require.resolve(`${name}/package.json`),metadata=JSON.parse(await readFile(file,"utf8")); + if(metadata.version!=="2.1.4")continue; + const binary=join(dirname(file),"bin",process.platform==="win32"?"opencode.exe":"opencode"); + const valid=await new Promise(resolve=>{ + const child=spawn(binary,["--version"],{stdio:"ignore",windowsHide:true}); + const timer=setTimeout(()=>{child.kill("SIGKILL");resolve(false)},5000); + child.on("error",()=>{clearTimeout(timer);resolve(false)});child.on("exit",code=>{clearTimeout(timer);resolve(code===0)}); + }); + if(valid)return binary; + }catch{} + } + fail("unsupported_runtime"); +} + +export async function createNativeCLI({root,providerConfig,request,history,tools,rawHistory,signal,onControl}){ + await assertNoUnmanagedConfig(); + const binary=await binaryPath(),channel=crypto.randomUUID(),password=crypto.randomUUID()+crypto.randomUUID(); + const paths=Object.fromEntries(["home","config","data","cache","state","tmp","work"].map(name=>[name,join(root,`cli-${name}`)])); + for(const path of Object.values(paths))await mkdir(path,{recursive:true}); + const plugin=join(paths.config,"host-capture");await mkdir(plugin,{recursive:true}); + await writeFile(join(plugin,"package.json"),JSON.stringify({name:"scopeweaver-host-capture",type:"module",main:"index.mjs"})); + await writeFile(join(plugin,"index.mjs"),await readFile(new URL("./native-plugin.mjs",import.meta.url))); + const input={request,history,tools,rawHistory,channel},inputPath=join(plugin,"bridge-input.json"); + const setRequest=async next=>{input.request=next;await writeFile(inputPath,JSON.stringify(input),{mode:0o600})}; + await setRequest(request); + const providers=structuredClone(providerConfig?.providers??{}),split=request.model.indexOf("/"),provider=request.model.slice(0,split),model=request.model.slice(split+1); + providers[provider]??={};providers[provider].models??={};providers[provider].models[model]??={}; + const selected=providers[provider].models[model]; + selected.body={...(request.thinking||request.thinking_type?{thinking:{type:request.thinking||request.thinking_type}}:{}),...(request.reasoning_effort?{reasoning_effort:request.reasoning_effort}:{})}; + selected.compatibility={...selected.compatibility,maxTokensField:selected.compatibility?.maxTokensField??"max_tokens"}; + const config={providers,plugins:[pathToFileURL(plugin).href],default_agent:"capture",agents:{capture:{mode:"primary",system:request.system.join("\n\n"),permissions:nativeRules}},permissions:nativeRules,update:"disable",share:"disabled",snapshots:false,warming:false,compaction:{auto:false}}; + const configPath=join(paths.config,"opencode.json");await writeFile(configPath,JSON.stringify(config),{mode:0o600}); + const env={PATH:process.env.PATH??"/usr/bin:/bin",PWD:paths.work,HOME:paths.home,USERPROFILE:paths.home,TMPDIR:paths.tmp,OPENCODE_TEST_HOME:paths.home,XDG_CONFIG_HOME:paths.config,XDG_DATA_HOME:paths.data,XDG_CACHE_HOME:paths.cache,XDG_STATE_HOME:paths.state,OPENCODE_CONFIG_DIR:paths.config,OPENCODE_CONFIG:configPath,OPENCODE_CONFIG_PROJECT_DISABLE:"1",OPENCODE_DISABLE_CLAUDE_CODE:"1",OPENCODE_PASSWORD:password}; + for(const key of ["SystemRoot","WINDIR","LANG","LC_ALL","TZ"])if(process.env[key])env[key]=process.env[key]; + const children=new Set();let client,runner,runnerExit,egress,closed=false; + const start=args=>{ + if(signal.aborted)throw new BridgeError("canceled",499); + const child=spawn(binary,args,{cwd:paths.work,env,windowsHide:true,stdio:["pipe","pipe","pipe"]});children.add(child); + child.stdin.on("error",()=>{});child.stderr.on("data",()=>{});child.once("close",()=>children.delete(child));return child; + }; + const close=async()=>{ + if(closed)return;closed=true;signal.removeEventListener("abort",abort); + egress?.stop(true); + for(const child of children){child.stdin.end();child.kill("SIGTERM")} + if(children.size)await Promise.race([Promise.all([...children].map(child=>new Promise(resolve=>child.once("close",resolve)))),new Promise(resolve=>setTimeout(resolve,200))]); + const remaining=[...children],exited=Promise.all(remaining.map(child=>new Promise(resolve=>child.once("close",resolve)))); + for(const child of remaining)child.kill("SIGKILL"); + if(remaining.length){ + let timer;try{await Promise.race([exited,new Promise((_,reject)=>{timer=setTimeout(()=>reject(new BridgeError("sdk_failure",502)),1000)})])}finally{clearTimeout(timer)} + } + }; + const abort=()=>{if(client)void client.session.interrupt({sessionID:activeSession,resume:false}).catch(()=>{});for(const child of children)child.kill("SIGTERM")}; + let activeSession; + signal.addEventListener("abort",abort,{once:true}); + try{ + const token=crypto.randomUUID()+crypto.randomUUID();let sent=false; + // Raw tool history becomes JSON inside JSON; allow its encoded expansion + // while the public bridge continues to enforce its 16 MiB input limit. + egress=Bun.serve({hostname:"127.0.0.1",port:0,idleTimeout:0,maxRequestBodySize:64*1024*1024,async fetch(incoming){ + if(incoming.method!=="POST"||new URL(incoming.url).pathname!=="/model"||incoming.headers.has("origin")||incoming.headers.get("x-scopeweaver-egress")!==token||sent)return new Response(null,{status:403}); + sent=true; + try{ + const target=new URL(incoming.headers.get("x-scopeweaver-upstream")??""); + if(target.username||target.password||target.hash||!["http:","https:"].includes(target.protocol))throw Error(); + const expected=providerConfig?new URL(providerConfig.providers[provider].settings.baseURL).origin:"https://opencode.ai"; + if(target.origin!==expected)throw Error(); + const headers=new Headers(incoming.headers); + for(const name of ["host","connection","content-length","x-scopeweaver-egress","x-scopeweaver-upstream"])headers.delete(name); + const response=await fetch(target,{method:"POST",headers,body:await incoming.arrayBuffer(),redirect:"error",signal:AbortSignal.any([signal,incoming.signal])}); + // Fetch exposes decoded bytes but retains the upstream encoding header. + // The downstream CLI must not attempt to decode those bytes again. + const received=new Headers(response.headers); + for(const name of ["content-encoding","content-length","transfer-encoding","connection"])received.delete(name); + return new Response(response.body,{status:response.status,headers:received}); + }catch{return new Response(null,{status:502})} + }}); + input.egress={url:`http://127.0.0.1:${egress.port}/model`,token};await setRequest(request); + const server=start(["serve","--stdio","--hostname","127.0.0.1","--port","0"]); + const url=await new Promise((resolve,reject)=>{ + let buffer="";const timer=setTimeout(()=>reject(new BridgeError("sdk_failure",502)),15000); + const done=(error,value)=>{clearTimeout(timer);error?reject(error):resolve(value)}; + server.stdout.on("data",chunk=>{ + buffer+=chunk.toString();if(buffer.length>1000000){done(new BridgeError("protocol_error",502));server.kill("SIGTERM");return} + let index;while((index=buffer.indexOf("\n"))>=0){ + const line=buffer.slice(0,index);buffer=buffer.slice(index+1);let value;try{value=JSON.parse(line)}catch{continue} + if(value._scopeweaver_native===channel){try{onControl(value.event)}catch{server.kill("SIGTERM")}} + else if(typeof value.url==="string"){ + const parsed=new URL(value.url);if(parsed.protocol!=="http:"||parsed.hostname!=="127.0.0.1"||parsed.username||parsed.password||parsed.pathname!=="/")done(new BridgeError("protocol_error",502));else done(undefined,value.url); + } + } + }); + server.once("error",()=>done(new BridgeError("sdk_failure",502)));server.once("exit",()=>done(new BridgeError(signal.aborted?"canceled":"sdk_failure",502))); + }); + client=OpenCode.make({baseUrl:url,headers:{Authorization:`Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`}}); + if((await client.server.info()).version!=="2.1.4")fail("unsupported_runtime"); + return {...client,directory:paths.work,setRequest,close,session:{...client.session,async prompt({sessionID}){ + activeSession=sessionID; + runner=start(["run","--server",url,"--session",sessionID,"--model",request.model,"--agent","capture","--format","json"]); + runnerExit=new Promise(resolve=>{runner.once("error",()=>resolve(-1));runner.once("exit",resolve)}); + let size=0;runner.stdout.on("data",chunk=>{size+=chunk.length;if(size>20000000){onControl({type:"failure",code:"protocol_error"});runner.kill("SIGTERM")}}); + runner.stdin.end("ScopeWeaver authoritative conversation is supplied by the host plugin."); + }},async waitRunner(){if(runnerExit&&(await runnerExit)!==0)fail("sdk_failure",502)}}; + }catch(error){await close();throw error} +} diff --git a/adapters/opencode-sdk/src/native-plugin.mjs b/adapters/opencode-sdk/src/native-plugin.mjs new file mode 100644 index 00000000..3acdd6de --- /dev/null +++ b/adapters/opencode-sdk/src/native-plugin.mjs @@ -0,0 +1,85 @@ +import { readFile } from "node:fs/promises"; + +// This local plugin is copied into the request's private config directory. +// Keep the real OpenCode tool registry and implementations; policy blocks execution. +const inputURL=new URL("./bridge-input.json",import.meta.url); +const input=JSON.parse(await readFile(inputURL,"utf8")); +const {request,history,tools,rawHistory,channel,egress}=input; +const exposed=new Set(tools.map(t=>t.name)); +const notify=event=>process.stdout.write(JSON.stringify({_scopeweaver_native:channel,event})+"\n"); +let network=0; + +export default {id:"scopeweaver.host-capture",async setup(ctx){ + await ctx.agent.transform(editor=>{ + for(const agent of editor.list())if(agent.id!=="capture")editor.remove(agent.id); + editor.default("capture"); + }); + await ctx.tool.transform(editor=>{ + const nativeNames=new Set(editor.list().map(t=>t.id)); + for(const tool of tools){ + if(nativeNames.has(tool.name)||tool.name==="execute")throw Error("HOST_TOOL_COLLISION"); + editor.add({...tool,options:{codemode:false,permission:"host_capture"},async execute(_,context){ + notify({type:"capture",id:context.id}); + return new Promise((_,reject)=>{ + const stop=()=>reject(Error("CAPTURE_INTERRUPTED")); + if(context.signal.aborted)stop();else context.signal.addEventListener("abort",stop,{once:true}); + }); + }}); + } + }); + await ctx.tool.hook("execute.before",event=>{ + if(!exposed.has(event.tool)){ + notify({type:"failure",code:"protocol_error"}); + throw Error("NATIVE_EXECUTION_DENIED"); + } + }); + await ctx.session.hook("context",async event=>{ + // Host instructions use the public custom-agent system setting. Keep the + // runtime's generated initial context; replace only conversation history. + event.messages=structuredClone(history); + event.system.push({type:"text",text:'ScopeWeaver tool policy: The host application owns tool execution. Use only direct function tools whose description starts with "Host tool:". All other OpenCode tools, including execute and native filesystem, shell, browser, and subagent tools, are disabled by policy. Do not call them. If no host tools are listed, respond without tools.'}); + const current=JSON.parse(await readFile(inputURL,"utf8")); + event.options.maxTokens=current.request.max_tokens; + if(request.temperature!==undefined)event.options.temperature=request.temperature; + if(request.stop!==undefined)event.options.stop=[...request.stop]; + if(request.reasoning_effort)event.options.reasoningEffort=request.reasoning_effort; + }); + await ctx.session.hook("http.request",async event=>{ + if(event.kind!=="primary"||++network!==1){ + notify({type:"failure",code:"second_model_request"}); + throw Error("SECOND_MODEL_REQUEST_BLOCKED"); + } + const body=await event.request.clone().json(); + const names=(body.tools??[]).map(t=>t.function?.name??t.name); + if(new Set(names).size!==names.length||[...exposed].some(name=>!names.includes(name)))throw Error("SDK_TOOL_CATALOG_CHANGED"); + if(Object.keys(rawHistory).length){ + const restored=new Set(); + for(const message of body.messages??[])for(const call of message.tool_calls??[])if(Object.hasOwn(rawHistory,call.id)){ + call.function.arguments=rawHistory[call.id];restored.add(call.id); + } + if(restored.size!==Object.keys(rawHistory).length)throw Error("SDK_HISTORY_FORMAT_UNSUPPORTED"); + const headers=new Headers(event.request.headers);headers.delete("content-length"); + event.request=new Request(event.request,{headers,body:JSON.stringify(body)}); + } + if(event.model.providerID==="opencode-go")event.request.headers.set("x-opencode-session",request.session_id??request.request_id); + else if(event.model.providerID!=="opencode")event.request.headers.delete("x-opencode-session"); + // The CLI's Effect transport drops Request.redirect. Route the unchanged + // genuine request through the worker's one-use, redirect-blocking transport. + const headers=new Headers(event.request.headers); + headers.set("x-scopeweaver-egress",egress.token); + headers.set("x-scopeweaver-upstream",event.request.url); + event.request=new Request(egress.url,{method:"POST",headers,body:await event.request.clone().arrayBuffer(),redirect:"error"}); + notify({type:"request"}); + }); + await ctx.session.hook("http.response",async event=>{ + if(event.kind!=="primary"||![400,402,403,413,429].includes(event.response.status)||!event.response.headers.get("content-type")?.includes("application/json"))return; + try{ + const text=await event.response.clone().text();if(text.length>65536)return; + const data=JSON.parse(text),code=data.error?.code??data.error?.type??data.error?.name,status=event.response.status; + const billing=["insufficient_quota","billing_hard_limit_reached","insufficient_balance","credit_balance_exhausted","credits_exhausted"]; + if([402,429].includes(status)&&(billing.includes(code)||(status===402&&code==="quota_exceeded")))notify({type:"evidence",code:"quota_exhausted"}); + if([400,413].includes(status)&&["context_length_exceeded","model_context_window_exceeded","context_window_exceeded","prompt_too_long"].includes(code))notify({type:"evidence",code:"context_length_exceeded"}); + if(status===403&&code==="FreeTierError")notify({type:"evidence",code:"free_tier_restricted"}); + }catch{} + }); +}}; diff --git a/adapters/opencode-sdk/src/native.mjs b/adapters/opencode-sdk/src/native.mjs new file mode 100644 index 00000000..c3828bcb --- /dev/null +++ b/adapters/opencode-sdk/src/native.mjs @@ -0,0 +1,168 @@ +import { OpenCode } from "@opencode/sdk"; +import { createNativeCLI, nativeRules } from "./native-cli.mjs"; +import { Config } from "@opencode/core/config"; +import { Global } from "@opencode/util/global"; +import { mkdir } from "node:fs/promises"; +import { BridgeError, fail, safeJSON, rawObject, aliasMap, nativeHistory, zeroPrice } from "./protocol.mjs"; + +const rules=nativeRules; +const abortError = () => new BridgeError("canceled",499); +const signature = native => { + const found=[]; + const walk=(v,d=0)=>{if(!v||typeof v!=="object"||d>3)return;for(const [k,x]of Object.entries(v)){if(["signature","encrypted"].includes(k)&&typeof x==="string")found.push(x);else if(typeof x==="object")walk(x,d+1)}}; + walk(native);const unique=[...new Set(found)];if(unique.length>1)fail("protocol_error");return unique[0]; +}; +function usage(tokens) { + const result={input_tokens:tokens?.input,output_tokens:tokens?.output,cache_read_tokens:tokens?.cache?.read??0,cache_write_tokens:tokens?.cache?.write??0}; + if(!Object.values(result).every(n=>Number.isSafeInteger(n)&&n>=0))fail("protocol_error"); + return result; +} +async function createHost(root, providerConfig, plugins=[], request) { + const paths=Object.fromEntries(["home","data","cache","config","state","tmp","bin","log","repos"].map(k=>[k,`${root}/${k}`])); + await mkdir(paths.config,{recursive:true}); + const providers=structuredClone(providerConfig?.providers??{}); + if(request) { + const split=request.model.indexOf("/");const provider=request.model.slice(0,split),model=request.model.slice(split+1); + providers[provider]??={};providers[provider].models??={};providers[provider].models[model]??={}; + const selected=providers[provider].models[model]; + selected.body={...(request.thinking||request.thinking_type?{thinking:{type:request.thinking||request.thinking_type}}:{}),...(request.reasoning_effort?{reasoning_effort:request.reasoning_effort}:{})}; + selected.compatibility={...selected.compatibility,maxTokensField:selected.compatibility?.maxTokensField??"max_tokens"}; + } + const content=JSON.stringify({providers,plugins:[],update:"disable",share:"disabled",snapshots:false,warming:false,compaction:{auto:false},permissions:rules,default_agent:"capture",agents:{capture:{mode:"primary",system:"",permissions:rules}}}); + return OpenCode.create({app:{name:"sdk",version:"2.1.4",channel:"latest"},database:{path:":memory:"},models:{fetch:!providerConfig,snapshot:!providerConfig},config:{directory:paths.config,project:false,claude:false,content},fs:{filewatcher:false,fff:false},plugins}, {overrides:[Global.node.replace(Global.layerWith(paths)),Config.node.replace(Config.configured({global:false,system:false,project:false,claude:false,content}))]}); +} +export async function discoverModels({root,providerConfig}) { + let publicIDs; + if(!providerConfig) { + try { + const response=await fetch("https://opencode.ai/inference/v1/models",{redirect:"error",signal:AbortSignal.timeout(10000)}); + if(!response.ok)throw Error(); + const catalog=safeJSON(await response.text(),"sdk_failure"); + if(!Array.isArray(catalog.data))throw Error(); + publicIDs=new Set(catalog.data.map(m=>m.id)); + } catch { fail("sdk_failure",502); } + } + const host=await createHost(root,providerConfig); + try { + const {data}=await host.model.list({location:{directory:`${root}/config`}}); + const models=data.filter(m=>providerConfig?Object.hasOwn(providerConfig.providers,m.providerID)&&Object.hasOwn(providerConfig.providers[m.providerID].models,m.id):m.providerID==="opencode"&&publicIDs.has(m.id)&&zeroPrice(m.cost)) + .map(m=>({id:`${m.providerID}/${m.id}`,name:m.name??m.id})).sort((a,b)=>a.id.localeCompare(b.id)); + if(!models.length)fail("sdk_failure",502); + return models; + } finally { await host.close(); } +} + +export async function completeNative(request,{root,providerConfig,rawHistory={},signal,emit}) { + // Validate against the original catalog, before a per-call model overlay can create a new model. + if(providerConfig) { + const split=request.model.indexOf("/"); + if(!Object.hasOwn(providerConfig.providers,request.model.slice(0,split))||!Object.hasOwn(providerConfig.providers[request.model.slice(0,split)].models,request.model.slice(split+1)))fail("unknown_model"); + } else if(!(await discoverModels({root:`${root}/catalog`,providerConfig})).some(m=>m.id===request.model))fail("unknown_model"); + const names=[...new Set([...request.tools.map(t=>t.name),...request.messages.flatMap(m=>m.content.filter(b=>b.type==="tool_use").map(b=>b.name))])]; + const forward=new Map(names.map((name,index)=>[name,`sw_tool_${index}`])),aliases={forward,reverse:new Map([...forward].map(([name,alias])=>[alias,name]))},history=nativeHistory(request,aliases); + const exposed=new Set(request.tools.map(t=>aliases.forward.get(t.name))); + const oldIDs=new Set(request.messages.flatMap(m=>m.content.filter(b=>b.type==="tool_use").map(b=>b.id))); + const calls=new Map(),rawChunks=new Map(),captures=new Set(),blocks=new Map(),content=[];let outputSize=0; + let host,session,consumer,error,ownedInterrupt=false,streamed=false,terminal=false,rawFinish,tokens,network=0,wake,quotaEvidence=false,contextEvidence=false,freeTierEvidence=false,eventsReady=false; + const eventAbort=new AbortController(); + const notify=()=>{wake?.();wake=undefined}; + const reject=(code="protocol_error")=>{error??=new BridgeError(code,502);notify()}; + const aborted=()=>{reject("canceled");if(session)void host.session.interrupt({sessionID:session.id,resume:false}).catch(()=>{})}; + signal.addEventListener("abort",aborted,{once:true}); + const wait=async predicate=>{while(!predicate()){if(signal.aborted)throw abortError();if(error)throw error;await new Promise(resolve=>{wake=resolve;if(predicate()||error||signal.aborted)notify()})}if(error)throw error;}; + const onControl=event=>{ + if(event.type==="capture"){captures.add(event.id);notify()} + else if(event.type==="request"){network++;if(network!==1)reject("second_model_request")} + else if(event.type==="failure")reject(event.code); + else if(event.type==="evidence"){ + if(event.code==="quota_exhausted")quotaEvidence=true; + if(event.code==="context_length_exceeded")contextEvidence=true; + if(event.code==="free_tier_restricted")freeTierEvidence=true; + } + }; + const consume=e=>{ + const d=e.data;if(!session||d?.sessionID!==session.id)return; + try { + if(e.type==="session.text.started"||e.type==="session.reasoning.started"){ + const type=e.type.includes("reasoning")?"thinking":"text",key=`${type}:${d.ordinal}`; + if(blocks.has(key))fail("protocol_error"); + const block={type,[type==="thinking"?"thinking":"text"]:""};blocks.set(key,block);content.push(block); + } else if(e.type==="session.text.delta"||e.type==="session.reasoning.delta") { + outputSize+=d.delta.length;if(outputSize>8000000)fail("protocol_error"); + const type=e.type.includes("reasoning")?"thinking":"text",key=`${type}:${d.ordinal}`; + if(!blocks.has(key)){const block={type,[type==="thinking"?"thinking":"text"]:""};blocks.set(key,block);content.push(block)} + blocks.get(key)[type==="thinking"?"thinking":"text"]+=d.delta; + emit({type:type==="thinking"?"thinking_delta":"text_delta",text:d.delta}); + } else if(e.type==="session.reasoning.ended") { + const sig=signature(d.native);if(sig){const b=blocks.get(`thinking:${d.ordinal}`);if(!b)fail("protocol_error");b.signature=sig;emit({type:"thinking_signature",text:sig})} + } else if(e.type==="session.tool.input.started") { + if(!exposed.has(d.name)||calls.has(d.id)||!d.id)fail("protocol_error"); + let index=calls.size,id=`sdk_${request.request_id}_${index}`; + while(oldIDs.has(id))id=`sdk_${request.request_id}_${++index}`; + oldIDs.add(id); + const call={type:"tool_use",id,name:aliases.reverse.get(d.name)};calls.set(d.id,call);content.push(call); + } else if(e.type==="session.tool.input.delta") { + if(!calls.has(d.id)||calls.get(d.id).raw!==undefined)fail("protocol_error"); + outputSize+=d.delta.length;if(outputSize>8000000)fail("protocol_error"); + rawChunks.set(d.id,(rawChunks.get(d.id)??"")+d.delta); + } else if(e.type==="session.tool.input.ended") { + const call=calls.get(d.id);if(!call||call.raw!==undefined)fail("protocol_error"); + if(rawChunks.has(d.id)&&rawChunks.get(d.id)!==d.text)fail("protocol_error"); + rawObject(d.text);call.raw=d.text; + } else if(e.type==="session.tool.called") {if(d.executed!==false||!calls.has(d.id))fail("protocol_error");} + else if(e.type==="session.tool.failed") {if(!ownedInterrupt||d.error?.type!=="aborted")fail("protocol_error");} + else if(e.type==="session.step.streamed")streamed=true; + else if(e.type==="session.step.ended"||e.type==="session.step.failed") { + if(e.type.endsWith("failed")&&!ownedInterrupt)fail("sdk_failure"); + tokens=usage(d.tokens);rawFinish=d.rawFinish??d.finish; + } else if(e.type==="session.execution.failed") { + reject(d.error?.message==="SECOND_MODEL_REQUEST_BLOCKED"?"second_model_request":"sdk_failure");terminal=true; + } else if(e.type==="session.execution.succeeded"||e.type==="session.execution.interrupted")terminal=true; + } catch(x) { reject(x.code); } + notify(); + }; + let result; + try { + if(signal.aborted)throw abortError(); + host=await createNativeCLI({root,providerConfig,request,history,rawHistory,signal,onControl,tools:request.tools.map(t=>({name:aliases.forward.get(t.name),description:`Host tool: ${JSON.stringify(t.name)}. Use this function when host instructions refer to that tool.\n\n${t.description}`,input:t.input_schema}))}); + const available=await host.model.list({location:{directory:host.directory}}); + const model=available.data.find(m=>`${m.providerID}/${m.id}`===request.model); + if(!model||(!providerConfig&&(model.providerID!=="opencode"||!zeroPrice(model.cost))))fail("unknown_model"); + if(model.limit?.output&&request.max_tokens>model.limit.output) { + // The requested upper bound is preserved when the model has a smaller hard limit. + request={...request,max_tokens:model.limit.output}; + } + await host.setRequest(request); + const split=request.model.indexOf("/"); + consumer=(async()=>{try{for await(const e of host.event.subscribe({signal:eventAbort.signal})){if(e.type==="server.connected"){eventsReady=true;notify()}else consume(e)}}catch{if(!eventAbort.signal.aborted)reject("sdk_failure")}})(); + await wait(()=>eventsReady); + session=await host.session.create({location:{directory:host.directory},agent:"capture",model:{providerID:request.model.slice(0,split),id:request.model.slice(split+1)},permissions:rules,title:"ScopeWeaver model request"}); + emit({type:"message_start"}); + await host.session.prompt({sessionID:session.id,text:"ScopeWeaver authoritative context is supplied by the host."}); + await wait(()=>streamed||terminal); + if(calls.size) { + await wait(()=>[...calls.entries()].every(([id,c])=>c.raw!==undefined&&captures.has(id))||terminal); + if(terminal||!calls.size)fail("protocol_error"); + ownedInterrupt=true; + await host.session.interrupt({sessionID:session.id,resume:false}); + } + await wait(()=>terminal); + if(!ownedInterrupt)await host.waitRunner(); + if(!tokens||network!==1)fail("protocol_error"); + const stop_reason=calls.size?"tool_use":["length","max_tokens","max-tokens"].includes(rawFinish)?"max_tokens":["stop","end_turn","end-turn"].includes(rawFinish)?"end_turn":undefined; + if(!stop_reason)fail("protocol_error"); + result={message:{role:"assistant",content},stop_reason,usage:tokens}; + } catch(e) { error=e instanceof BridgeError?e:new BridgeError(signal.aborted?"canceled":"sdk_failure",502); } + finally { + if(session){try{await host.session.interrupt({sessionID:session.id,resume:false});await host.session.wait({sessionID:session.id});await host.session.remove({sessionID:session.id})}catch{error??=new BridgeError("sdk_failure",502)}} + eventAbort.abort();if(consumer)await consumer; + if(host){try{await host.close()}catch{error??=new BridgeError("sdk_failure",502)}} + signal.removeEventListener("abort",aborted); + } + if(error){if(error.code!=="canceled"){if(quotaEvidence)throw new BridgeError("quota_exhausted",402);if(contextEvidence)throw new BridgeError("context_length_exceeded",400);if(freeTierEvidence)throw new BridgeError("free_tier_restricted",403)}throw error;} + if(signal.aborted)throw abortError(); + // No tool request becomes visible to the host until native cleanup has completed. + for(const call of calls.values()){emit({type:"tool_use_start",tool_id:call.id,tool_name:call.name});emit({type:"tool_input_delta",text:call.raw})} + emit({type:"message_delta",stop_reason:result.stop_reason,usage:result.usage});emit({type:"message_stop"}); + return result; +} diff --git a/adapters/opencode-sdk/src/protocol.mjs b/adapters/opencode-sdk/src/protocol.mjs new file mode 100644 index 00000000..b3755000 --- /dev/null +++ b/adapters/opencode-sdk/src/protocol.mjs @@ -0,0 +1,160 @@ +export class BridgeError extends Error { + constructor(code, status = 400) { super(code); this.code = code; this.status = status; } +} +export const fail = (code, status) => { throw new BridgeError(code, status); }; +export const zeroPrice = cost => Array.isArray(cost)&&cost.length>0&&cost.every(c=>c?.input===0&&c?.output===0); +export const messages = Object.freeze({ + unauthorized: "Bridge authentication required.", forbidden: "Only non-browser loopback requests are accepted.", + invalid_request: "Invalid completion request.", unsupported_input: "This input cannot be represented safely by the SDK.", + unsupported_runtime: "Unsupported SDK/runtime pair or platform. Use a tested pinned runtime.", + unknown_model: "The selected model is unavailable in this bridge.", duplicate_request: "This request ID is already active.", + canceled: "The request was canceled.", timeout: "The SDK request timed out.", + sdk_failure: "The SDK request failed. Check the selected model and provider configuration.", + protocol_error: "The SDK response could not be represented safely.", configuration_error: "Invalid bridge provider configuration.", + second_model_request: "An unexpected additional SDK model request was blocked.", + quota_exhausted: "The upstream provider reported exhausted quota or billing credit.", + context_length_exceeded: "The upstream model reported that the context length was exceeded.", + free_tier_restricted: "The hosted free tier is restricted for this SDK runtime.", + not_found: "Route not found.", body_too_large: "Request body exceeds the bridge limit.", +}); +export const publicError = error => { + const code = Object.hasOwn(messages, error?.code) ? error.code : "sdk_failure"; + return { code, message: messages[code] }; +}; +export function safeJSON(text, code = "invalid_request") { + try { + return JSON.parse(text, (_, value) => { + if (typeof value === "number" && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value)))) throw Error(); + if (value && typeof value === "object" && !Array.isArray(value) && Object.hasOwn(value, "__proto__")) throw Error(); + return value; + }); + } catch { fail(code); } +} +export function rawObject(text) { + if(typeof text!=="string"||text.length>2000000)fail("protocol_error"); + let value;try{value=JSON.parse(text)}catch{fail("protocol_error")} + if(!value||typeof value!=="object"||Array.isArray(value))fail("protocol_error"); + const errors=[],tree=parseTree(text,errors,{disallowComments:true,allowTrailingComma:false}); + if(!tree||errors.length)fail("protocol_error"); + const walk=node=>{if(node.type==="object"){const keys=new Set();for(const p of node.children??[]){const key=p.children[0].value;if(keys.has(key)||key==="__proto__")fail("protocol_error");keys.add(key)}}for(const c of node.children??[])walk(c)};walk(tree); + return value; +} +export function parseRequest(text) { + const errors=[],tree=parseTree(text,errors,{disallowComments:true,allowTrailingComma:false}); + if(!tree||errors.length)fail("invalid_request"); + const rawNodes=new Set(),rawHistory=Object.create(null); + // Tool arguments are authoritative raw JSON. Their JS projection is never executed. + let request;try{request=JSON.parse(text)}catch{fail("invalid_request")} + for(const [i,m]of (Array.isArray(request.messages)?request.messages:[]).entries())for(const [j,b]of (Array.isArray(m?.content)?m.content:[]).entries())if(b?.type==="tool_use"){ + const node=findNodeAtLocation(tree,["messages",i,"content",j,"input"]); + if(node){rawNodes.add(node);rawHistory[b.id]=text.slice(node.offset,node.offset+node.length)} + } + const walk=(node,raw=false)=>{ + raw=raw||rawNodes.has(node); + if(node.type==="object"){ + const keys=new Set();for(const p of node.children??[]){const key=p.children[0].value;if(keys.has(key)||key==="__proto__")fail("invalid_request");keys.add(key)} + } + if(!raw&&node.type==="number"&&(!Number.isFinite(node.value)||(Number.isInteger(node.value)&&!Number.isSafeInteger(node.value))))fail("unsupported_input"); + for(const c of node.children??[])walk(c,raw); + };walk(tree); + return {request:validateRequest(request),rawHistory}; +} +const object = x => x && typeof x === "object" && !Array.isArray(x); +const str = (x, max = 16*1024*1024) => typeof x === "string" && x.length <= max; +const knownKeys = (x, allowed) => Object.keys(x).every(k => allowed.includes(k)); +export function validateRequest(x) { + if (!object(x) || !knownKeys(x, ["request_id","session_id","model","system","messages","tools","max_tokens","temperature","stop","thinking","thinking_type","reasoning_effort"])) fail("invalid_request"); + if (!str(x.request_id, 128) || !/^[A-Za-z0-9_-]+$/.test(x.request_id) || !str(x.model, 512) || !/^[^/\s]+\/\S+$/.test(x.model)) fail("invalid_request"); + if(x.session_id!==undefined&&(!str(x.session_id,256)||!x.session_id||/[\r\n\x00-\x1f\x7f]/.test(x.session_id)))fail("invalid_request"); + if (!Array.isArray(x.system) || !x.system.every(s => str(s)) || !Array.isArray(x.messages) || !x.messages.length || x.messages.length > 1000 || !Array.isArray(x.tools) || x.tools.length > 128) fail("invalid_request"); + if (!Number.isSafeInteger(x.max_tokens) || x.max_tokens < 1 || x.max_tokens > 1000000) fail("invalid_request"); + if (x.temperature !== undefined && (typeof x.temperature !== "number" || !Number.isFinite(x.temperature) || x.temperature < 0 || x.temperature > 2)) fail("invalid_request"); + if (x.stop !== undefined && (!Array.isArray(x.stop) || x.stop.length > 16 || !x.stop.every(s => str(s, 4096) && s.length))) fail("invalid_request"); + for (const k of ["thinking", "thinking_type"]) if (x[k] !== undefined && !["", "enabled", "disabled"].includes(x[k])) fail("unsupported_input"); + if (x.reasoning_effort !== undefined && !["", "none", "minimal", "low", "medium", "high", "xhigh", "max"].includes(x.reasoning_effort)) fail("unsupported_input"); + const names = new Set(); + for (const t of x.tools) { + if (!object(t) || !knownKeys(t,["name","description","input_schema"]) || !str(t.name, 512) || !t.name || names.has(t.name) || !str(t.description, 32000) || !object(t.input_schema)) fail("invalid_request"); + schemaCheck(t.input_schema); names.add(t.name); + } + const calls = new Map(), results = new Set(); + for (const m of x.messages) { + if (!object(m) || !knownKeys(m,["role","content"]) || !["assistant","user"].includes(m.role) || !Array.isArray(m.content)) fail("invalid_request"); + for (const b of m.content) { + if (!object(b)) fail("invalid_request"); + if (b.type === "text") { if (!knownKeys(b,["type","text"]) || !str(b.text)) fail("invalid_request"); } + else if (b.type === "thinking") { if (m.role !== "assistant" || !knownKeys(b,["type","thinking","signature"]) || !str(b.thinking) || (b.signature !== undefined && !str(b.signature))) fail("invalid_request"); } + else if (b.type === "tool_use") { + if (m.role !== "assistant" || !knownKeys(b,["type","id","name","input"]) || !str(b.id,256) || !b.id || calls.has(b.id) || !str(b.name,512) || !b.name || !object(b.input)) fail("invalid_request"); + calls.set(b.id,b.name); + } else if (b.type === "tool_result") { + if (m.role !== "user" || !knownKeys(b,["type","tool_use_id","content","is_error"]) || !calls.has(b.tool_use_id) || results.has(b.tool_use_id) || !Array.isArray(b.content) || !b.content.every(c => object(c) && knownKeys(c,["type","text"]) && c.type === "text" && str(c.text)) || (b.is_error !== undefined && typeof b.is_error !== "boolean")) fail("unsupported_input"); + results.add(b.tool_use_id); + } else fail("unsupported_input"); + } + } + if ([...calls.keys()].some(id => !results.has(id))) fail("unsupported_input"); + return x; +} +function schemaCheck(value, depth = 0, root = value) { + if (depth > 32) fail("unsupported_input"); + if (Array.isArray(value)) { for (const v of value) schemaCheck(v, depth+1,root); } + else if (object(value)) { + if (Object.keys(value).length > 1000 || ["$dynamicRef", "$id"].some(k => Object.hasOwn(value,k))) fail("unsupported_input"); + if(Object.hasOwn(value,"$ref")) { + if(typeof value.$ref!=="string"||(value.$ref!=="#"&&!value.$ref.startsWith("#/")))fail("unsupported_input"); + let target=root; + for(const part of value.$ref==="#"?[]:value.$ref.slice(2).split("/")) { + if(/~(?![01])/.test(part))fail("unsupported_input"); + const key=part.replaceAll("~1","/").replaceAll("~0","~"); + if(!target||typeof target!=="object"||!Object.hasOwn(target,key))fail("unsupported_input");target=target[key]; + } + } + for (const v of Object.values(value)) schemaCheck(v, depth+1,root); + } +} +export function aliasMap(request) { + const names = [...request.tools.map(t=>t.name), ...request.messages.flatMap(m=>m.content.filter(b=>b.type === "tool_use").map(b=>b.name))]; + const unique=[...new Set(names)],valid=name=>/^[A-Za-z0-9_-]{1,64}$/.test(name),reserved=new Set(unique.filter(valid)),forward=new Map();let index=0; + for(const name of unique){let alias=name;if(!valid(name)){do{alias=`sw_tool_${index++}`}while(reserved.has(alias));reserved.add(alias)}forward.set(name,alias)} + return { forward, reverse: new Map([...forward].map(([k,v])=>[v,k])) }; +} +export function nativeHistory(request, aliases) { + const names = new Map(); const out=[]; + const thinking = request.thinking || request.thinking_type; + for (const m of request.messages) { + let content=[]; + const flush = () => { if(content.length)out.push({role:m.role,content}); content=[]; }; + for (const b of m.content) { + if(b.type === "text") content.push({type:"text",text:b.text}); + else if(b.type === "thinking") { if(thinking !== "disabled")content.push({type:"reasoning",text:b.thinking,...(b.signature?{encrypted:b.signature}:{})}); } + else if(b.type === "tool_use") { const name=aliases.forward.get(b.name);names.set(b.id,name);content.push({type:"tool-call",id:b.id,name,input:b.input}); } + else { flush();out.push({role:"tool",content:[{type:"tool-result",id:b.tool_use_id,name:names.get(b.tool_use_id),result:{type:b.is_error?"error":"text",value:b.content.map(t=>t.text).join("\n")}}]}); } + } + flush(); + } + return out; +} +export function serializeComplete(result) { + const blocks=result.message.content.map(b=>b.type === "tool_use" + ? `{"type":"tool_use","id":${JSON.stringify(b.id)},"name":${JSON.stringify(b.name)},"input":${b.raw}}` + : JSON.stringify(b)); + return `{"message":{"role":"assistant","content":[${blocks.join(",")}]},"stop_reason":${JSON.stringify(result.stop_reason)},"usage":${JSON.stringify(result.usage)}}`; +} +export function validateProviderConfig(config) { + if (config === undefined) return undefined; + if (!object(config) || !knownKeys(config,["providers"]) || !object(config.providers) || !Object.keys(config.providers).length) fail("configuration_error"); + for(const [id,p] of Object.entries(config.providers)) { + if(!/^[A-Za-z0-9_-]+$/.test(id) || !object(p) || !knownKeys(p,["name","package","settings","models"]) || p.package !== "@opencode/ai/providers/openai-compatible" || !object(p.settings) || !knownKeys(p.settings,["baseURL","apiKey","timeout"]) || !object(p.models))fail("configuration_error"); + let url;try{url=new URL(p.settings.baseURL)}catch{fail("configuration_error")} + if(!["http:","https:"].includes(url.protocol)||url.username||url.password||url.search||url.hash)fail("configuration_error"); + if(p.settings.apiKey!==undefined&&!str(p.settings.apiKey,8192))fail("configuration_error"); + for(const [name,m] of Object.entries(p.models)) { + if(!name||!object(m)||!knownKeys(m,["name","limit","capabilities","compatibility"]))fail("configuration_error"); + if(m.limit!==undefined&&(!object(m.limit)||!knownKeys(m.limit,["context","input","output"])||!Object.values(m.limit).every(n=>Number.isSafeInteger(n)&&n>0)))fail("configuration_error"); + if(m.compatibility!==undefined&&(!object(m.compatibility)||!knownKeys(m.compatibility,["maxTokensField","reasoningField"])||(m.compatibility.maxTokensField!==undefined&&!["max_tokens","max_completion_tokens"].includes(m.compatibility.maxTokensField))))fail("configuration_error"); + } + } + return structuredClone(config); +} +import { parseTree, findNodeAtLocation } from "jsonc-parser"; diff --git a/adapters/opencode-sdk/src/server.mjs b/adapters/opencode-sdk/src/server.mjs new file mode 100644 index 00000000..e73dbf24 --- /dev/null +++ b/adapters/opencode-sdk/src/server.mjs @@ -0,0 +1,15 @@ +import { readFile } from "node:fs/promises"; +import { createBridge, MAX_REQUEST_BYTES } from "./bridge.mjs"; +import { safeJSON } from "./protocol.mjs"; +const host=process.env.SCOPEWEAVER_OPENCODE_HOST??"127.0.0.1"; +if(host!=="127.0.0.1")throw new Error("The OpenCode bridge must listen on 127.0.0.1"); +const port=Number(process.env.SCOPEWEAVER_OPENCODE_PORT??4097); +if(!Number.isInteger(port)||port<1||port>65535)throw new Error("Invalid bridge port"); +const configPath=process.env.SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG; +const timeout=Number(process.env.SCOPEWEAVER_OPENCODE_TIMEOUT_MS??0); +const bridge=createBridge({token:process.env.SCOPEWEAVER_OPENCODE_TOKEN,providerConfig:configPath?safeJSON(await readFile(configPath,"utf8"),"configuration_error"):undefined,runtime:process.env.SCOPEWEAVER_OPENCODE_RUNTIME??"native",timeout}); +const server=Bun.serve({hostname:host,port,idleTimeout:0,maxRequestBodySize:MAX_REQUEST_BYTES,fetch:req=>bridge.fetch(req)}); +console.log(`ScopeWeaver OpenCode SDK bridge listening on http://${host}:${server.port}`); +let stopping=false; +const stop=async()=>{if(stopping)return;stopping=true;server.stop();await bridge.close();process.exit(0)}; +process.on("SIGTERM",stop);process.on("SIGINT",stop); diff --git a/adapters/opencode-sdk/src/worker.mjs b/adapters/opencode-sdk/src/worker.mjs new file mode 100644 index 00000000..dc627774 --- /dev/null +++ b/adapters/opencode-sdk/src/worker.mjs @@ -0,0 +1,22 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fail, publicError } from "./protocol.mjs"; +const abort=new AbortController(); +// This process handles one call. Set policy before lazily importing either SDK. +const platformFetch=globalThis.fetch; +globalThis.fetch=(input,init)=>platformFetch(input,{...init,redirect:"error"}); +process.on("SIGTERM",()=>abort.abort());process.on("SIGINT",()=>abort.abort()); +const write=value=>process.stdout.write(JSON.stringify(value)+"\n"); +let root; +try { + const input=JSON.parse(await new Response(Bun.stdin.stream()).text()); + root=input.root??await mkdtemp(join(tmpdir(),"scopeweaver-opencode-")); + const runtime=input.runtime??"native";let discover,complete; + if(runtime==="native"){const backend=await import("./native.mjs");discover=backend.discoverModels;complete=backend.completeNative} + else if(runtime==="legacy"){const backend=await import("./legacy.mjs");discover=backend.discoverLegacyModels;complete=backend.completeLegacy} + else fail("unsupported_runtime"); + if(input.mode==="models")write({kind:"models",models:await discover({root,providerConfig:input.providerConfig})}); + else write({kind:"result",result:await complete(input.request,{root,rawHistory:input.rawHistory,providerConfig:input.providerConfig,signal:abort.signal,emit:event=>write({kind:"event",event})})}); +} catch(e){write({kind:"error",...publicError(e)});process.exitCode=1;} +finally{if(root)await rm(root,{recursive:true,force:true});} diff --git a/adapters/opencode-sdk/test/legacy.test.mjs b/adapters/opencode-sdk/test/legacy.test.mjs new file mode 100644 index 00000000..ee7d9e72 --- /dev/null +++ b/adapters/opencode-sdk/test/legacy.test.mjs @@ -0,0 +1,350 @@ +import { test, expect, beforeAll, afterAll } from "bun:test"; +import http from "node:http"; +import { mkdtemp, rm, readFile, mkdir, writeFile } from "node:fs/promises"; +import { createHash } from "node:crypto"; +import { fileURLToPath } from "node:url"; +import { execFileSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { existsSync } from "node:fs"; +import { completeLegacy, discoverLegacyModels } from "../src/legacy.mjs"; +import legacyPlugin from "../src/legacy-plugin.mjs"; +import { createBridge } from "../src/bridge.mjs"; +import { parseRequest } from "../src/protocol.mjs"; +let dependencySnapshot; +async function dependencyDigest() { + const root = fileURLToPath(new URL("../", import.meta.url)), paths = []; + for await (const path of new Bun.Glob("**/package.json").scan({ cwd: `${root}/node_modules`, onlyFiles: true, followSymlinks: false })) paths.push(`node_modules/${path}`); + paths.push("package.json", "bun.lock"); paths.sort(); + const hash = createHash("sha256"); + for (const path of paths) { hash.update(path); hash.update(await readFile(`${root}/${path}`)); } + return hash.digest("hex"); +} +beforeAll(async () => { dependencySnapshot = await dependencyDigest(); }); +afterAll(async () => { expect(await dependencyDigest()).toBe(dependencySnapshot); }); +function cliPIDs() { + return new Set(execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8" }).split("\n") + .filter(line => /opencode-[^\s/]+\/bin\/opencode serve --hostname 127\.0\.0\.1/.test(line)).map(line => Number(line.trim().split(/\s+/)[0]))); +} +function noNewCLI(before) { expect([...cliPIDs()].filter(pid => !before.has(pid))).toEqual([]); } +const tool = { name: "host.read", description: "Capture only", input_schema: { type: "object", properties: { path: { type: "string" } }, required: ["path"], additionalProperties: false } }; +const request = (extra = {}) => ({ request_id: crypto.randomUUID(), session_id: "host-session", model: "fixture/test", system: ["HOST SYSTEM EXACT"], messages: [{ role: "user", content: [{ type: "text", text: "HOST USER EXACT" }] }], tools: [tool], max_tokens: 37, temperature: 0.2, stop: ["END"], thinking: "disabled", ...extra }); + +async function fixture(mode, callback) { + const root = await mkdtemp(`${tmpdir()}/scopeweaver-test-legacy-`), requests = [], events = []; + let markStarted; const started = new Promise(resolve => { markStarted = resolve; }); + const server = http.createServer(async (req, res) => { + let text = ""; for await (const chunk of req) text += chunk; + if (req.url === "/v1/models") { res.writeHead(200, { "content-type": "application/json" }); res.end('{"data":[{"id":"test"}]}'); return; } + const body = JSON.parse(text); requests.push(body); markStarted(); + if (mode === "redirect" && req.url !== "/escaped") { res.writeHead(307, { location: `http://127.0.0.1:${server.address().port}/escaped` }); res.end(); return; } + if ([500, 429, 402].includes(mode)) { res.writeHead(mode, { "content-type": "application/json" }); res.end(mode === 402 ? '{"error":{"code":"insufficient_quota"}}' : '{"error":{"message":"fixture failure"}}'); return; } + if (mode === "quota429") { res.writeHead(429, { "content-type": "application/json" }); res.end('{"error":{"code":"insufficient_quota"}}'); return; } + if (["context400", "context413", "generic400"].includes(mode)) { + res.writeHead(mode === "context413" ? 413 : 400, { "content-type": "application/json" }); + res.end(JSON.stringify({ error: mode === "context400" ? { code: "context_length_exceeded" } : mode === "context413" ? { type: "prompt_too_long" } : { code: "invalid_request_error", message: "context length exceeded" } })); return; + } + if (["freetier403", "generic403"].includes(mode)) { + res.writeHead(403, { "content-type": "application/json" }); + res.end(JSON.stringify({ error: mode === "freetier403" ? { name: "FreeTierError", data: { message: "fixture restriction" } } : { code: "forbidden", message: "FreeTierError" } })); return; + } + if (mode === "hang") return; + res.writeHead(200, { "content-type": "text/event-stream" }); + const emit = (delta, finish = null, usage) => res.write(`data: ${JSON.stringify({ id: "fixture", object: "chat.completion.chunk", created: 1, model: "test", choices: [{ index: 0, delta, finish_reason: finish }], ...(usage ? { usage } : {}) })}\n\n`); + emit({ role: "assistant", content: "native text" }); + if (mode !== "text" && (requests.length === 1 || mode === "reuse")) { + const name = body.tools?.[0]?.function?.name; + const count = mode === "two" ? 2 : 1; + for (let i = 0; i < count; i++) emit({ tool_calls: [{ index: i, id: `native-call-${i}`, type: "function", function: { name: mode === "evil" ? "bash" : name, arguments: mode === "unsafe" ? '{"id":9223372036854775807}' : mode === "evil" ? JSON.stringify({ command: `touch '${root}/NATIVE_BASH_RAN'` }) : JSON.stringify({ path: `file-${i}.txt` }) } }] }); + emit({}, "tool_calls", { prompt_tokens: 101, completion_tokens: 13, total_tokens: 114, prompt_tokens_details: { cached_tokens: 5 } }); + } else emit({}, "stop", { prompt_tokens: 101, completion_tokens: 13, total_tokens: 114, prompt_tokens_details: { cached_tokens: 5 } }); + res.end("data: [DONE]\n\n"); + }); + await mkdir(`${root}/.opencode/plugins`, { recursive: true }); + await writeFile(`${root}/.opencode/plugins/hostile.mjs`, `import {writeFileSync} from 'node:fs';export default async()=>{writeFileSync(${JSON.stringify(`${root}/PROJECT_PLUGIN_RAN`)},'bad');return {}}`); + await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); + const providerConfig = { providers: { fixture: { name: "Fixture", package: "@opencode/ai/providers/openai-compatible", settings: { baseURL: `http://127.0.0.1:${server.address().port}/v1`, apiKey: "fixture-no-secret" }, models: { test: { name: "Fixture model", limit: { context: 200000, output: 777 }, compatibility: { maxTokensField: "max_tokens" } } } } } }; + try { return await callback({ root, providerConfig, requests, events, started, emit: e => events.push(e), signal: AbortSignal.timeout(45000) }); } + finally { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); const loaded = existsSync(`${root}/PROJECT_PLUGIN_RAN`); await rm(root, { recursive: true, force: true }); expect(loaded).toBe(false); } +} + +test("legacy actual CLI captures two tools, streams, gates the second step and forwards frozen settings", async () => { + await fixture("two", async context => { + const result = await completeLegacy(request({ reasoning_effort: "low" }), context); + expect(context.requests).toHaveLength(1); + expect(context.requests[0]).toMatchObject({ max_tokens: 37, temperature: 0.2, stop: ["END"], thinking: { type: "disabled" }, reasoning_effort: "low" }); + expect(context.requests[0].tools.map(t => t.function.name)).toEqual(["capture_host_0"]); + expect(context.requests[0].tools[0].function.description).toContain(tool.name); + expect(context.requests[0].tools[0].function.description).toContain(tool.description); + expect(JSON.stringify(context.requests[0].messages)).toContain("HOST USER EXACT"); + expect(context.requests[0].messages.filter(message => message.role === "system").map(message => message.content).join("\n")).toBe("HOST SYSTEM EXACT"); + expect(result.stop_reason).toBe("tool_use"); + expect(result.message.content.filter(b => b.type === "tool_use").map(b => b.raw)).toEqual(['{"path":"file-0.txt"}', '{"path":"file-1.txt"}']); + expect(result.usage).toEqual({ input_tokens: 96, output_tokens: 13, cache_read_tokens: 5, cache_write_tokens: 0 }); + expect(context.events.filter(e => e.type === "text_delta").map(e => e.text).join("")).toBe("native text"); + expect(context.events.at(-1).type).toBe("message_stop"); + }); +}, 30000); + +test("legacy discovers configured real models without inference and completes a tool-free turn", async () => { + await fixture("text", async context => { + expect(await discoverLegacyModels(context)).toEqual([{ id: "fixture/test", name: "Fixture model" }]); + expect(context.requests).toHaveLength(0); + const result = await completeLegacy(request({ tools: [] }), { ...context, root: `${context.root}/complete` }); + expect(result.stop_reason).toBe("end_turn"); expect(context.requests).toHaveLength(1); + expect(context.requests[0].tools ?? []).toHaveLength(0); + }); +}, 30000); + +test("legacy preserves host call/result IDs and safe JSON history while rejecting unsafe numbers", async () => { + await fixture("text", async context => { + const messages = [{ role: "user", content: [{ type: "text", text: "old user" }] }, { role: "assistant", content: [{ type: "tool_use", id: "host-call", name: tool.name, input: { path: "old" } }] }, { role: "user", content: [{ type: "tool_result", tool_use_id: "host-call", is_error: true, content: [{ type: "text", text: "HOST REAL FAILURE" }] }] }]; + await completeLegacy(request({ messages }), { ...context, rawHistory: { "host-call": '{"path":"old"}' } }); + const wire = JSON.stringify(context.requests[0].messages); + expect(wire).toContain("host-call"); expect(wire).toContain("HOST REAL FAILURE"); + await completeLegacy(request({ messages }), { ...context, root: `${context.root}/whitespace`, rawHistory: { "host-call": '{ "path": "old" }' } }); + const call = context.requests[1].messages.flatMap(message => message.tool_calls ?? []).find(call => call.id === "host-call"); + expect(JSON.parse(call.function.arguments)).toEqual({ path: "old" }); + await expect(completeLegacy(request({ messages }), { ...context, root: `${context.root}/int64`, rawHistory: { "host-call": '{"id":9223372036854775807}' } })).rejects.toMatchObject({ code: "unsupported_input" }); + expect(context.requests).toHaveLength(2); + }); +}, 30000); + +const safeHistoryEncodings = [ + // Exact input object captured from product QA's real Go encoding/json request. + ["Go HTML escaping", String.raw`{"command":"printf 'gh49-allow-proof\\n' \u003e\u003e /tmp/scopeweaver-gh49-product-qa/app-legacy/allow.txt"}`, "printf 'gh49-allow-proof\\n' >> /tmp/scopeweaver-gh49-product-qa/app-legacy/allow.txt"], + ["whitespace and escaped Unicode", String.raw`{ + "command" : "printf '\ud55c\uad6d\\n' \u003e\u003e /tmp/legacy-unicode.txt" + }`, "printf '한국\\n' >> /tmp/legacy-unicode.txt"], +]; +for (const [name, raw, command] of safeHistoryEncodings) test(`legacy continues actual host history with ${name}`, async () => { + await fixture("text", async context => { + const input = { command }, callID = "real-go-bash-call"; + const messages = [ + { role: "user", content: [{ type: "text", text: "run the allowed command" }] }, + { role: "assistant", content: [{ type: "tool_use", id: callID, name: "Bash", input }] }, + { role: "user", content: [{ type: "tool_result", tool_use_id: callID, content: [{ type: "text", text: "REAL HOST RESULT" }] }] }, + ]; + const serialized = JSON.stringify(request({ messages, tools: [] })).replace(JSON.stringify(input), raw); + const parsed = parseRequest(serialized); + expect(parsed.rawHistory[callID]).toBe(raw); + const result = await completeLegacy(parsed.request, { ...context, rawHistory: parsed.rawHistory }); + expect(result.stop_reason).toBe("end_turn"); expect(context.requests).toHaveLength(1); + const call = context.requests[0].messages.flatMap(message => message.tool_calls ?? []).find(call => call.id === callID); + expect(JSON.parse(call.function.arguments)).toEqual(input); + expect(JSON.parse(call.function.arguments).command).toBe(command); + expect(JSON.stringify(context.requests[0].messages)).toContain("REAL HOST RESULT"); + }); +}, 30000); + +test("legacy rejects unsafe Go rawHistory numbers with zero model HTTP requests", async () => { + await fixture("text", async context => { + const input = { id: 1 }, callID = "go-int64-call"; + const messages = [ + { role: "user", content: [{ type: "text", text: "old user" }] }, + { role: "assistant", content: [{ type: "tool_use", id: callID, name: tool.name, input }] }, + { role: "user", content: [{ type: "tool_result", tool_use_id: callID, content: [{ type: "text", text: "old result" }] }] }, + ]; + const serialized = JSON.stringify(request({ messages })).replace(JSON.stringify(input), '{"id":9223372036854775807}'); + const parsed = parseRequest(serialized); + await expect(completeLegacy(parsed.request, { ...context, rawHistory: parsed.rawHistory })).rejects.toMatchObject({ code: "unsupported_input" }); + expect(context.requests).toHaveLength(0); expect(existsSync(`${context.root}/legacy`)).toBe(false); + }); +}, 30000); + +for (const thinking of ["enabled", "disabled"]) test(`legacy rejects opaque reasoning history before inference with thinking ${thinking}`, async () => { + await fixture("text", async context => { + const messages = [ + { role: "user", content: [{ type: "text", text: "old user" }] }, + { role: "assistant", content: [{ type: "thinking", thinking: "old thought", signature: "opaque-signature" }, { type: "text", text: "old answer" }] }, + { role: "user", content: [{ type: "text", text: "continue" }] }, + ]; + let error; + try { await completeLegacy(request({ messages, tools: [], thinking }), context); } catch (caught) { error = caught; } + expect(context.requests).toHaveLength(0); + expect(existsSync(`${context.root}/legacy`)).toBe(false); + expect(error).toMatchObject({ code: "unsupported_input" }); + }); +}, 30000); + +test("legacy keeps unsigned reasoning history supported through the actual converter", async () => { + await fixture("text", async context => { + const messages = [ + { role: "user", content: [{ type: "text", text: "old user" }] }, + { role: "assistant", content: [{ type: "thinking", thinking: "UNSIGNED HISTORY" }, { type: "text", text: "old answer" }] }, + { role: "user", content: [{ type: "text", text: "continue" }] }, + ]; + const result = await completeLegacy(request({ messages, tools: [], thinking: "enabled" }), context); + expect(result.stop_reason).toBe("end_turn"); expect(context.requests).toHaveLength(1); + expect(context.requests[0].messages.some(message => message.role === "assistant" && message.reasoning_content === "UNSIGNED HISTORY")).toBe(true); + }); +}, 30000); + +test("legacy reuses a native call ID across two host turns without reusing host IDs", async () => { + await fixture("reuse", async context => { + const first = await completeLegacy(request(), context); + const call = first.message.content.find(block => block.type === "tool_use"); + const next = request({ messages: [ + ...request().messages, + { role: "assistant", content: [{ type: "tool_use", id: call.id, name: call.name, input: JSON.parse(call.raw) }] }, + { role: "user", content: [{ type: "tool_result", tool_use_id: call.id, is_error: false, content: [{ type: "text", text: "REAL HOST RESULT" }] }] }, + ] }); + const second = await completeLegacy(next, { ...context, root: `${context.root}/second`, rawHistory: { [call.id]: call.raw } }); + const nextCall = second.message.content.find(block => block.type === "tool_use"); + expect(nextCall.id).not.toBe(call.id); expect(context.requests).toHaveLength(2); + expect(JSON.stringify(context.requests[1].messages)).toContain(call.id); + expect(JSON.stringify(context.requests[1].messages)).toContain("REAL HOST RESULT"); + }); +}, 45000); + +for (const status of [500, 429]) test(`legacy ${status} cannot retry a second upstream network request`, async () => { + await fixture(status, async context => { + await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "second_model_request" }); + expect(context.requests).toHaveLength(1); expect(context.events.some(e => e.type === "tool_use_start")).toBe(false); + }); +}, 30000); + +test("legacy structured 402 quota remains distinct from generic 429", async () => { + await fixture(402, async context => { + await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "quota_exhausted" }); + expect(context.requests).toHaveLength(1); + }); +}, 30000); + +test("legacy retains structured 429 quota through the native retry boundary", async () => { + await fixture("quota429", async context => { + await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "quota_exhausted" }); + expect(context.requests).toHaveLength(1); + }); +}, 30000); + +for (const mode of ["context400", "context413"]) test(`legacy preserves structured ${mode} for host compaction`, async () => { + await fixture(mode, async context => { + await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "context_length_exceeded", status: 400 }); + expect(context.requests).toHaveLength(1); expect(context.events.some(event => event.type === "tool_use_start")).toBe(false); + }); +}, 30000); + +test("legacy does not classify generic 400 prose as context overflow", async () => { + await fixture("generic400", async context => { + await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "sdk_failure" }); + expect(context.requests).toHaveLength(1); + }); +}, 30000); + +test("legacy preserves structured FreeTierError as an explicit runtime restriction", async () => { + await fixture("freetier403", async context => { + await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "free_tier_restricted", status: 403 }); + expect(context.requests).toHaveLength(1); expect(context.events.some(event => event.type === "tool_use_start")).toBe(false); + }); +}, 30000); + +test("legacy does not classify generic 403 prose as a free tier restriction", async () => { + await fixture("generic403", async context => { + await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "sdk_failure" }); + expect(context.requests).toHaveLength(1); + }); +}, 30000); + +test("legacy retains redirect failure through both pre-network retry gates", async () => { + const root = await mkdtemp(`${tmpdir()}/scopeweaver-test-legacy-redirect-`), paths = []; + const server = http.createServer((req, res) => { + paths.push(req.url); + res.writeHead(307, { location: `http://127.0.0.1:${server.address().port}/escaped` }); res.end(); + }); + await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); + try { + const state = `${root}/state.json`; + await writeFile(state, JSON.stringify({ provider: "fixture", request: request(), allowed: [], history: [] })); + const hooks = await legacyPlugin({}, { state }), config = {}; + await hooks.config(config); + await hooks["chat.params"]({ sessionID: "redirect" }, { options: {} }); + const fetch = config.provider.fixture.options.fetch; + await expect(fetch(`http://127.0.0.1:${server.address().port}/model`)).rejects.toMatchObject({ code: "UnexpectedRedirect" }); + await expect(hooks["chat.params"]({ sessionID: "redirect" }, { options: {} })).rejects.toThrow("SCOPEWEAVER_LEGACY_REDIRECT"); + await expect(fetch(`http://127.0.0.1:${server.address().port}/model`)).rejects.toThrow("SCOPEWEAVER_LEGACY_REDIRECT"); + expect(paths).toEqual(["/model"]); + } finally { + server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); + await rm(root, { recursive: true, force: true }); + } +}); + +test("legacy refuses model redirects before the redirected request leaves the CLI", async () => { + await fixture("redirect", async context => { + await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "sdk_failure" }); + expect(context.requests).toHaveLength(1); + expect(context.events.some(event => event.type === "tool_use_start")).toBe(false); + }); +}, 30000); + +test("legacy rejects an unadvertised native bash call without publishing host tools", async () => { + await fixture("evil", async context => { + await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "protocol_error" }); + expect(context.requests).toHaveLength(1); expect(context.events.some(e => e.type === "tool_use_start")).toBe(false); + expect(existsSync(`${context.root}/NATIVE_BASH_RAN`)).toBe(false); + }); +}, 30000); + +test("legacy cancellation aborts owned inference and concurrent peer remains isolated", async () => { + await Promise.all([fixture("hang", async context => { + const controller = new AbortController(); + const completion = completeLegacy(request(), { ...context, signal: controller.signal }); + await context.started; + controller.abort(); await expect(completion).rejects.toMatchObject({ code: "canceled" }); + expect(context.requests).toHaveLength(1); + }), fixture("two", async context => { + const result = await completeLegacy(request({ max_tokens: 19 }), context); + expect(result.stop_reason).toBe("tool_use"); expect(context.requests[0].max_tokens).toBe(19); expect(context.requests).toHaveLength(1); + })]); +}, 60000); + +test("legacy rejects unsafe numeric output before delivering tool calls", async () => { + await fixture("unsafe", async context => { + await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "protocol_error" }); + expect(context.events.some(e => e.type === "tool_use_start")).toBe(false); expect(context.requests).toHaveLength(1); + }); +}, 30000); + +test("legacy bridge preserves local schema refs and retains the host tool contract", async () => { + await fixture("two", async context => { + const token = "legacy-ref-token-01234567890123456789"; + const bridge = createBridge({ token, runtime: "legacy", providerConfig: context.providerConfig }); + try { + const schema = { type: "object", $defs: { path: { type: "string" } }, properties: { path: { $ref: "#/$defs/path" } }, required: ["path"], additionalProperties: false }; + const response = await bridge.fetch(new Request("http://127.0.0.1:4097/v1/complete", { method: "POST", headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(request({ tools: [{ ...tool, input_schema: schema }] })) })); + expect(response.status).toBe(200); + const result = await response.json(); + expect(result.message.content.filter(block => block.type === "tool_use")).toHaveLength(2); + expect(context.requests[0].tools[0].function.parameters).toMatchObject(schema); + expect(context.requests).toHaveLength(1); + } finally { await bridge.close(); } + }); +}, 30000); + +test("legacy cancellation during startup cannot create an orphan CLI", async () => { + await fixture("hang", async context => { + const before = cliPIDs(), controller = new AbortController(); + const pending = completeLegacy(request(), { ...context, signal: controller.signal }); + setTimeout(() => controller.abort(), 20); + await expect(pending).rejects.toMatchObject({ code: "canceled" }); + noNewCLI(before); expect(context.requests).toHaveLength(0); + }); +}, 30000); + +for (const mode of ["disconnect", "shutdown"]) test(`legacy bridge ${mode} stops a blocked model and owned CLI`, async () => { + await fixture("hang", async context => { + const before = cliPIDs(), token = "legacy-test-token-01234567890123456789"; + const bridge = createBridge({ token, runtime: "legacy", providerConfig: context.providerConfig }); + const response = await bridge.fetch(new Request("http://127.0.0.1:4097/v1/stream", { method: "POST", headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(request()) })); + expect(response.status).toBe(200); + const reader = response.body.getReader(); + try { + await reader.read(); await context.started; + expect([...cliPIDs()].filter(pid => !before.has(pid)).length).toBeGreaterThan(0); + if (mode === "disconnect") await reader.cancel(); + await bridge.close(); + if (mode === "shutdown") await reader.cancel(); + noNewCLI(before); expect(context.requests).toHaveLength(1); + } finally { await reader.cancel().catch(() => {}); await bridge.close(); } + }); +}, 30000); diff --git a/adapters/opencode-sdk/test/native.test.mjs b/adapters/opencode-sdk/test/native.test.mjs new file mode 100644 index 00000000..e62920c0 --- /dev/null +++ b/adapters/opencode-sdk/test/native.test.mjs @@ -0,0 +1,234 @@ +import { describe, test, expect } from "bun:test"; +import { mkdtemp, stat, rm, writeFile, symlink } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createBridge, MAX_REQUEST_BYTES, workerEnvironment } from "../src/bridge.mjs"; +import { aliasMap, nativeHistory, safeJSON, validateRequest, zeroPrice } from "../src/protocol.mjs"; + +import { assertNoUnmanagedConfig, unmanagedConfigPaths } from "../src/native-cli.mjs"; + +const token="test-bridge-token-with-at-least-32-characters"; +const auth={authorization:`Bearer ${token}`,"content-type":"application/json"}; +const request=(changes={})=>({request_id:crypto.randomUUID(),model:"fixture/model",system:["Exact host system", "Second segment"],messages:[{role:"user",content:[{type:"text",text:"host question"}]}],tools:[],max_tokens:64,thinking:"disabled",...changes}); +const tool=(name="read/file")=>({name,description:"Host owned action",input_schema:{type:"object",properties:{n:{type:"number"},text:{type:"string"}},required:["n","text"],additionalProperties:false}}); +const fetchBridge=(bridge,path,body,extra={})=>bridge.fetch(new Request(`http://127.0.0.1:4097${path}`,{method:body?"POST":"GET",headers:auth,...extra,...(body?{body:JSON.stringify(body)}:{})})); +async function fixture(mode="text",providerID="fixture",timeout=20000) { + const requests=[],headers=[],hits=[];let canceled=false; + const root=await mkdtemp(join(tmpdir(),"gh49-native-test-")),marker=join(root,"MUST_NOT_EXIST"); + const server=Bun.serve({hostname:"127.0.0.1",port:0,async fetch(req){ + hits.push(req.url);if(req.method!=="POST")return new Response("redirect reached",{status:500}); + const body=await req.json();requests.push(body);headers.push(Object.fromEntries(req.headers)); + if(mode==="redirect")return Response.redirect(`http://127.0.0.1:${server.port}/redirect-target`,302); + if(mode==="quota")return Response.json({error:{code:"insufficient_quota",message:"secret billing details"}},{status:402}); + if(mode==="rate")return Response.json({error:{code:"rate_limit_exceeded",message:"transient"}},{status:429}); + if(mode==="context")return Response.json({error:{code:"context_length_exceeded",message:"private request details"}},{status:400}); + if(mode==="generic400")return Response.json({error:{code:"invalid_request_error",message:"context_length_exceeded quota"}},{status:400}); + if(mode==="free-tier")return Response.json({error:{name:"FreeTierError",data:{message:"private policy details"}}},{status:403}); + if(mode==="generic403")return Response.json({error:{code:"permission_denied",message:"FreeTierError quota"}},{status:403}); + if(mode==="failure"||mode==="retry")return new Response("untrusted secret provider error",{status:mode==="retry"?500:401}); + if(mode==="hang"||(mode==="selective"&&body.messages.some(m=>m.role==="user"&&m.content.includes("please hang"))))return new Response(new ReadableStream({cancel(){canceled=true}}),{headers:{"content-type":"text/event-stream"}}); + const finish=mode==="multi"&&body.messages.at(-1)?.content==="finish"; + const names=(body.tools??[]).filter(t=>t.function.description?.startsWith("Host tool: ")).map(t=>t.function.name); + const calls=mode==="unknown"?[{id:"evil",name:"shell",raw:JSON.stringify({command:`touch ${marker}`})}]:mode==="unsafe"?[{id:"call1",name:names[0],raw:'{"n":9007199254740993,"text":"x"}'}]:mode==="badjson"?[{id:"call1",name:names[0],raw:'{"n":1,"text":"x",}'}]:mode==="schema"?[{id:"call1",name:names[0],raw:'{"n":"bad","text":"x"}'}]:(mode==="tools"||(mode==="multi"&&!finish))?[{id:"call1",name:names[0],raw:'{ "n": 1.0, "text": "한글" }'},{id:"call2",name:names[1],raw:'{"n":2,"text":"second"}'}]:[]; + const chunks=[{choices:[{index:0,delta:{role:"assistant",reasoning_content:"native thought"},finish_reason:null}]},{choices:[{index:0,delta:{content:mode==="oversize"?"x".repeat(8000001):"native answer"},finish_reason:null}]}]; + for(const [index,call] of calls.entries()) { + chunks.push({choices:[{index:0,delta:{tool_calls:[{index,id:call.id,type:"function",function:{name:call.name,arguments:call.raw.slice(0,5)}}]},finish_reason:null}]}); + chunks.push({choices:[{index:0,delta:{tool_calls:[{index,function:{arguments:call.raw.slice(5)}}]},finish_reason:null}]}); + } + chunks.push({choices:[{index:0,delta:{},finish_reason:calls.length?"tool_calls":"stop"}],usage:{prompt_tokens:101,completion_tokens:23,total_tokens:124,prompt_tokens_details:{cached_tokens:5,cache_write_tokens:3}}}); + if(mode==="gzip"){const sse=chunks.map(c=>`data: ${JSON.stringify({id:"fixture",object:"chat.completion.chunk",created:1,model:"model",...c})}\n\n`).join("")+"data: [DONE]\n\n";return new Response(Bun.gzipSync(sse),{headers:{"content-type":"text/event-stream","content-encoding":"gzip"}})} + return new Response(new ReadableStream({async start(output){try{for(const c of chunks){output.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({id:"fixture",object:"chat.completion.chunk",created:1,model:"model",...c})}\n\n`));await new Promise(r=>setTimeout(r,5))}output.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));output.close()}catch{}},cancel(){canceled=true}}),{headers:{"content-type":"text/event-stream"}}); + }}); + const providerConfig={providers:{[providerID]:{name:"Fixture",package:"@opencode/ai/providers/openai-compatible",settings:{baseURL:`http://127.0.0.1:${server.port}/v1`,apiKey:"explicit-fixture-only"},models:{model:{name:"Fixture model",capabilities:{tools:true},limit:{context:200000,output:32768},compatibility:{maxTokensField:"max_tokens"}}}}}}; + const bridge=createBridge({token,providerConfig,timeout}); + return {bridge,requests,headers,hits,marker,get canceled(){return canceled},async close(){await bridge.close();server.stop(true);await rm(root,{recursive:true,force:true})}}; +} + +describe("bridge boundary",()=>{ + test("native CLI refuses unmanaged configuration entries, including broken symlinks",async()=>{ + const root=await mkdtemp(join(tmpdir(),"native-config-policy-"));try{ + expect(unmanagedConfigPaths("linux")).toEqual(["/etc/opencode"]); + const absent=join(root,"absent");await assertNoUnmanagedConfig([absent]); + const file=join(root,"managed");await writeFile(file,"{}");await expect(assertNoUnmanagedConfig([file])).rejects.toMatchObject({code:"configuration_error"}); + if(process.platform!=="win32"){const link=join(root,"broken");await symlink(absent,link);await expect(assertNoUnmanagedConfig([link])).rejects.toMatchObject({code:"configuration_error"})} + }finally{await rm(root,{recursive:true,force:true})} + }); + test("auth, browser origin, loopback, runtime and malformed input fail before worker",async()=>{ + let invoked=0;const bridge=createBridge({token,worker:async()=>{invoked++;return []}}); + expect((await bridge.fetch(new Request("http://127.0.0.1/v1/capabilities"))).status).toBe(401); + expect((await fetchBridge(bridge,"/v1/capabilities",null,{headers:{...auth,origin:"null"}})).status).toBe(403); + expect((await bridge.fetch(new Request("http://example.com/v1/capabilities",{headers:auth}))).status).toBe(403); + expect((await fetchBridge(bridge,"/v1/capabilities?x=1")).status).toBe(403); + expect((await fetchBridge(bridge,"/v1/complete",request({max_tokens:0}))).status).toBe(400); + expect((await fetchBridge(bridge,"/v1/capabilities")).status).toBe(200); + expect((await (await fetchBridge(bridge,"/v1/capabilities")).json()).runtime).toBe("native"); + expect((await bridge.fetch(new Request("http://127.0.0.1/v1/complete",{method:"POST",headers:{...auth,"content-length":String(MAX_REQUEST_BYTES+1)},body:"{}"}))).status).toBe(413); + const legacy=createBridge({token,runtime:"legacy1",worker:async()=>{invoked++}}); + expect((await fetchBridge(legacy,"/v1/models")).status).toBe(400); + expect(invoked).toBe(0);await bridge.close();await legacy.close(); + }); + test("unsafe history numbers and unsupported references fail closed",()=>{ + expect(()=>safeJSON('{"n":9007199254740993}')).toThrow(); + expect(()=>validateRequest(request({tools:[{...tool(),input_schema:{$ref:"https://example.com/schema"}}]}))).toThrow(); + expect(()=>validateRequest(request({messages:[{role:"user",content:[{type:"image"}]}]}))).toThrow(); + }); + test("unknown and empty pricing never imply free eligibility",()=>{ + for(const c of [undefined,[],[{}],[{input:0}],[{input:0,output:1}]])expect(zeroPrice(c)).toBe(false); + expect(zeroPrice([{input:0,output:0}])).toBe(true); + }); + test("worker environments never inherit cloud credentials, proxies or user config",()=>{ + const env=workerEnvironment("/tmp/owned-worker",{PATH:"/bin",LANG:"en_US.UTF-8",OPENCODE_API_KEY:"private",AWS_SECRET_ACCESS_KEY:"private",HOME:"/real-home",HTTP_PROXY:"http://proxy",BUN_OPTIONS:"--preload unwanted",NODE_OPTIONS:"--require unwanted",XDG_CONFIG_HOME:"/user-config"}); + expect(env.PATH).toBe("/bin");expect(env.LANG).toBe("en_US.UTF-8");expect(env.XDG_CONFIG_HOME).toBe("/tmp/owned-worker/xdg-config"); + for(const key of ["OPENCODE_API_KEY","AWS_SECRET_ACCESS_KEY","HOME","HTTP_PROXY","BUN_OPTIONS","NODE_OPTIONS"])expect(env[key]).toBeUndefined(); + }); + test("valid host names remain meaningful in tool-free history and aliases never collide",()=>{ + const input=request({tools:[tool("Bash"),tool("set_goals"),tool("sw_tool_0"),tool("read/file")]});const aliases=aliasMap(input); + expect(aliases.forward.get("Bash")).toBe("Bash");expect(aliases.forward.get("set_goals")).toBe("set_goals");expect(aliases.forward.get("sw_tool_0")).toBe("sw_tool_0");expect(aliases.forward.get("read/file")).toBe("sw_tool_1"); + }); + test("a four-MiB host context fits the documented input boundary",async()=>{ + let length=0;const bridge=createBridge({token,worker:async input=>{length=input.request.system[0].length;return {message:{role:"assistant",content:[{type:"text",text:"ok"}]},stop_reason:"end_turn",usage:{input_tokens:0,output_tokens:0,cache_read_tokens:0,cache_write_tokens:0}}}}); + try{expect((await fetchBridge(bridge,"/v1/complete",request({system:["x".repeat(4*1024*1024)]}))).status).toBe(200);expect(length).toBe(4*1024*1024)}finally{await bridge.close()} + }); + test("host tool correlation and roles stay exact in native history",()=>{ + const input=validateRequest(request({tools:[tool()],messages:[{role:"user",content:[{type:"text",text:"question"}]},{role:"assistant",content:[{type:"thinking",thinking:"old thought",signature:"old-signature"},{type:"tool_use",id:"old-id",name:"read/file",input:{n:1,text:"old"}}]},{role:"user",content:[{type:"tool_result",tool_use_id:"old-id",content:[{type:"text",text:"host result"}],is_error:true},{type:"text",text:"continue"}]}]})); + const native=nativeHistory(input,aliasMap(input)); + expect(native[1].content).toEqual([{type:"tool-call",id:"old-id",name:"sw_tool_0",input:{n:1,text:"old"}}]); + expect(native[2]).toEqual({role:"tool",content:[{type:"tool-result",id:"old-id",name:"sw_tool_0",result:{type:"error",value:"host result"}}]}); + expect(native[3]).toEqual({role:"user",content:[{type:"text",text:"continue"}]}); + }); +}); + +describe("actual native 2.1.4 runtime",()=>{ + test("model list is inference-free; exact system/history/settings and cache usage survive",async()=>{ + const f=await fixture();try{ + const models=await (await fetchBridge(f.bridge,"/v1/models")).json();expect(models.models).toEqual([{id:"fixture/model",name:"Fixture model"}]);expect(f.requests).toHaveLength(0); + const res=await fetchBridge(f.bridge,"/v1/complete",request({temperature:0.25,stop:["STOP"]}));const body=await res.json(); + expect(res.status).toBe(200);expect(body.stop_reason).toBe("end_turn");expect(body.usage).toEqual({input_tokens:93,output_tokens:23,cache_read_tokens:5,cache_write_tokens:3}); + expect(f.requests).toHaveLength(1);const wire=f.requests[0];expect(wire.max_tokens).toBe(64);expect(wire.max_completion_tokens).toBeUndefined();expect(wire.temperature).toBe(0.25);expect(wire.stop).toEqual(["STOP"]);expect(wire.thinking).toEqual({type:"disabled"}); + expect(wire.messages[0].role).toBe("system");expect(wire.messages[0].content.startsWith("Exact host system\n\nSecond segment\n")).toBe(true);expect(wire.messages[0].content).toContain("Here is some useful information about the environment");expect(wire.messages.slice(1)).toEqual([{role:"user",content:"host question"}]);expect(wire.tools.map(t=>t.function.name)).toContain("shell");expect(wire.tools.filter(t=>t.function.description?.startsWith("Host tool: "))).toEqual([]); + expect(f.headers[0]["x-opencode-session"]).toBeUndefined();expect(f.headers[0].authorization).toBe("Bearer explicit-fixture-only");expect(f.headers[0]["x-scopeweaver-egress"]).toBeUndefined();expect(f.headers[0]["x-scopeweaver-upstream"]).toBeUndefined(); + }finally{await f.close()} + }); + test("native egress forwards compressed SSE without a second content decoding",async()=>{ + const f=await fixture("gzip");try{ + const res=await fetchBridge(f.bridge,"/v1/complete",request()),body=await res.json();expect(res.status).toBe(200);expect(body.message.content.find(b=>b.type==="text").text).toBe("native answer");expect(f.requests).toHaveLength(1); + }finally{await f.close()} + }); + test("multiple capture tools retain names/order/raw arguments; NDJSON stops once after cleanup",async()=>{ + const f=await fixture("tools");try{ + const input=request({tools:[tool("read/file"),tool("other.tool")]}),res=await fetchBridge(f.bridge,"/v1/stream",input); + const events=(await res.text()).trim().split("\n").map(JSON.parse); + expect(events.find(e=>e.type==="error")).toBeUndefined();expect(events.filter(e=>e.type==="message_stop")).toHaveLength(1); + expect(events.filter(e=>e.type==="tool_use_start").map(e=>[e.tool_id,e.tool_name])).toEqual([[`sdk_${input.request_id}_0`,"read/file"],[`sdk_${input.request_id}_1`,"other.tool"]]); + expect(events.filter(e=>e.type==="tool_input_delta").map(e=>e.text)).toEqual(['{ "n": 1.0, "text": "한글" }','{"n":2,"text":"second"}']); + expect(events.find(e=>e.type==="thinking_delta").text).toBe("native thought");expect(events.find(e=>e.type==="text_delta").text).toBe("native answer");expect(f.requests).toHaveLength(1); + const hostTools=f.requests[0].tools.filter(t=>t.function.description?.startsWith("Host tool: "));expect(hostTools.map(t=>t.function.name)).toEqual(["sw_tool_0","sw_tool_1"]); + expect(hostTools[0].function.description).toContain('Host tool: "read/file"');expect(hostTools[0].function.description.endsWith(tool().description)).toBe(true); + }finally{await f.close()} + }); + test("complete retains raw numeric argument representation",async()=>{ + const f=await fixture("tools");try{const res=await fetchBridge(f.bridge,"/v1/complete",request({tools:[tool(),tool("second")]}));const raw=await res.text();expect(res.status).toBe(200);expect(raw).toContain('"input":{ "n": 1.0, "text": "한글" }')}finally{await f.close()} + }); + for(const mode of ["unknown","schema","badjson","failure","retry","redirect","oversize"])test(`${mode} fails without host tool delivery or native action`,async()=>{ + const f=await fixture(mode);try{ + const events=(await (await fetchBridge(f.bridge,"/v1/stream",request({tools:[tool()]}))).text()).trim().split("\n").map(JSON.parse); + expect(events.some(e=>e.type==="error")).toBe(true);expect(events.some(e=>e.type==="tool_use_start"||e.type==="message_stop")).toBe(false); + expect(f.requests).toHaveLength(1);expect(f.hits).toHaveLength(1);expect(await stat(f.marker).then(()=>true,()=>false)).toBe(false);expect(JSON.stringify(events)).not.toContain("untrusted secret provider error"); + }finally{await f.close()} + }); + test("concurrent per-call settings and tool catalogs are isolated",async()=>{ + const f=await fixture();try{ + const responses=await Promise.all([fetchBridge(f.bridge,"/v1/complete",request({max_tokens:37,temperature:0.1})),fetchBridge(f.bridge,"/v1/complete",request({max_tokens:73,temperature:0.9,thinking:"enabled",reasoning_effort:"low",tools:[tool()]}))]); + expect(responses.map(r=>r.status)).toEqual([200,200]);expect(f.requests.map(r=>r.max_tokens).sort((a,b)=>a-b)).toEqual([37,73]);expect(f.requests.find(r=>r.max_tokens===37).temperature).toBe(0.1);expect(f.requests.find(r=>r.max_tokens===73).temperature).toBe(0.9);expect(f.requests.find(r=>r.max_tokens===73).tools.filter(t=>t.function.description?.startsWith("Host tool: "))).toHaveLength(1);expect(f.requests.find(r=>r.max_tokens===37).tools.filter(t=>t.function.description?.startsWith("Host tool: "))).toHaveLength(0);expect(f.requests.find(r=>r.max_tokens===73).reasoning_effort).toBe("low"); + }finally{await f.close()} + }); + test("DELETE aborts an active SDK worker; peer request remains usable",async()=>{ + const f=await fixture("selective");try{ + const input=request({messages:[{role:"user",content:[{type:"text",text:"please hang"}]}]}),res=await fetchBridge(f.bridge,"/v1/stream",input);const reader=res.body.getReader();await reader.read(); + expect((await fetchBridge(f.bridge,"/v1/complete",request())).status).toBe(200); + expect((await fetchBridge(f.bridge,`/v1/requests/${input.request_id}`,null,{method:"DELETE"})).status).toBe(200); + let output="";while(true){const x=await reader.read();if(x.done)break;output+=new TextDecoder().decode(x.value)}expect(output).toContain('"type":"error"');expect(output).not.toContain('"type":"message_stop"'); + expect((await fetchBridge(f.bridge,"/v1/capabilities")).status).toBe(200); + }finally{await f.close()} + }); + test("host tool-result history is injected without native conversation growth",async()=>{ + const f=await fixture();try{ + const req=request({tools:[tool()],messages:[{role:"user",content:[{type:"text",text:"first"}]},{role:"assistant",content:[{type:"tool_use",id:"prior",name:"read/file",input:{n:7,text:"old"}}]},{role:"user",content:[{type:"tool_result",tool_use_id:"prior",content:[{type:"text",text:"host result"}]},{type:"text",text:"next"}]}]}); + const res=await fetchBridge(f.bridge,"/v1/complete",req);expect(res.status).toBe(200); + const wire=f.requests[0].messages;expect(wire).toHaveLength(5);expect(wire[2].tool_calls[0]).toEqual({id:"prior",type:"function",function:{name:"sw_tool_0",arguments:'{"n":7,"text":"old"}'}});expect(wire[3]).toEqual({role:"tool",tool_call_id:"prior",content:"host result"});expect(wire[4]).toEqual({role:"user",content:"next"}); + }finally{await f.close()} + }); + test("unknown explicit model is rejected before inference",async()=>{ + const f=await fixture();try{expect((await fetchBridge(f.bridge,"/v1/complete",request({model:"fixture/missing"}))).status).toBe(400);expect(f.requests).toHaveLength(0)}finally{await f.close()} + }); + test("HTTP response cancellation cleans an owned SDK worker",async()=>{ + const f=await fixture("hang");try{const res=await fetchBridge(f.bridge,"/v1/stream",request());const reader=res.body.getReader();await reader.read();await reader.cancel();await f.bridge.close()}finally{await f.close()} + }); + test("gateway session header stays stable across fresh native sessions",async()=>{ + const f=await fixture("text","opencode-go");try{ + const a=request({model:"opencode-go/model",session_id:"host-session"}),b=request({model:"opencode-go/model",session_id:"host-session"});expect(a.request_id).not.toBe(b.request_id); + expect((await fetchBridge(f.bridge,"/v1/complete",a)).status).toBe(200);expect((await fetchBridge(f.bridge,"/v1/complete",b)).status).toBe(200); + expect(f.headers.map(h=>h["x-opencode-session"])).toEqual(["host-session","host-session"]); + expect(f.headers.map(h=>h["user-agent"])).toEqual([f.headers[0]["user-agent"],f.headers[0]["user-agent"]]); + }finally{await f.close()} + }); + test("OpenCode provider preserves its genuine native session identity",async()=>{ + const f=await fixture("text","opencode");try{ + expect((await fetchBridge(f.bridge,"/v1/complete",request({model:"opencode/model",session_id:"host-session"}))).status).toBe(200); + expect(f.headers[0]["x-opencode-session"]).not.toBe("host-session");expect(f.headers[0]["x-opencode-session"]).toStartWith("ses_"); + expect(f.headers[0]["user-agent"]).toBe("opencode/latest/2.1.4/cli");expect(f.headers[0]["x-opencode-client"]).toBe("cli"); + }finally{await f.close()} + }); + test("64-bit tool IDs retain authoritative raw arguments in stream and complete",async()=>{ + const f=await fixture("unsafe");try{ + const response=await fetchBridge(f.bridge,"/v1/complete",request({tools:[tool()]}));expect(response.status).toBe(200);expect(await response.text()).toContain('"input":{"n":9007199254740993,"text":"x"}'); + const events=(await (await fetchBridge(f.bridge,"/v1/stream",request({tools:[tool()]}))).text()).trim().split("\n").map(JSON.parse);expect(events.find(e=>e.type==="tool_input_delta").text).toBe('{"n":9007199254740993,"text":"x"}');expect(events.filter(e=>e.type==="message_stop")).toHaveLength(1); + }finally{await f.close()} + }); + test("64-bit host history is restored exactly before the native HTTP request",async()=>{ + const f=await fixture();try{ + const input=request({tools:[tool()],messages:[{role:"assistant",content:[{type:"tool_use",id:"old64",name:"read/file",input:{n:0,text:"raw"}}]},{role:"user",content:[{type:"tool_result",tool_use_id:"old64",content:[{type:"text",text:"host result"}]}]}]}); + const raw=JSON.stringify(input).replace('"n":0','"n":9007199254740993'); + const res=await f.bridge.fetch(new Request("http://127.0.0.1:4097/v1/complete",{method:"POST",headers:auth,body:raw}));expect(res.status).toBe(200);expect(f.requests[0].messages[1].tool_calls[0].function.arguments).toBe('{"n":9007199254740993,"text":"raw"}'); + }finally{await f.close()} + }); + test("local JSON Pointer refs, defs and unions work in actual native tool schemas",async()=>{ + const f=await fixture("tools");try{ + const first=tool();first.input_schema={$defs:{Number:{anyOf:[{type:"number"},{type:"string"}]}},type:"object",properties:{n:{$ref:"#/$defs/Number"},text:{type:"string"}},required:["n","text"],additionalProperties:false}; + const res=await fetchBridge(f.bridge,"/v1/complete",request({tools:[first,tool("other")]}));expect(res.status).toBe(200);expect(f.requests[0].tools.find(t=>t.function.name==="sw_tool_0").function.parameters).toEqual(first.input_schema); + }finally{await f.close()} + }); + test("structured quota exhaustion is distinct from transient rate limiting",async()=>{ + for(const mode of ["quota","rate"]){const f=await fixture(mode);try{ + const res=await fetchBridge(f.bridge,"/v1/complete",request()),body=await res.json(); + expect(body.error.code==="quota_exhausted").toBe(mode==="quota");if(mode==="quota")expect(res.status).toBe(402); + expect(JSON.stringify(body)).not.toContain("secret billing details");expect(f.requests).toHaveLength(1); + }finally{await f.close()}} + }); + test("reused upstream call IDs remain unique across real host tool turns",async()=>{ + const f=await fixture("multi");try{ + const history=[{role:"user",content:[{type:"text",text:"host question"}]}],allIDs=[]; + for(const text of ["continue","finish"]){ + const input=request({messages:structuredClone(history),tools:[tool(),tool("other")]}),response=await fetchBridge(f.bridge,"/v1/complete",input),result=await response.json();expect(response.status).toBe(200); + const calls=result.message.content.filter(b=>b.type==="tool_use");expect(calls).toHaveLength(2);allIDs.push(...calls.map(c=>c.id)); + history.push(result.message,{role:"user",content:[...calls.map(c=>({type:"tool_result",tool_use_id:c.id,content:[{type:"text",text:"host approved result"}]})),{type:"text",text}]}); + } + expect(new Set(allIDs).size).toBe(4); + const last=await fetchBridge(f.bridge,"/v1/complete",request({messages:history,tools:[tool(),tool("other")]}));expect(last.status).toBe(200);expect((await last.json()).stop_reason).toBe("end_turn");expect(f.requests).toHaveLength(3); + expect(f.requests[2].messages.filter(m=>m.role==="tool").map(m=>m.tool_call_id)).toEqual(allIDs); + }finally{await f.close()} + }); + test("an explicit deadline is bounded independently of host cancellation",async()=>{ + const f=await fixture("hang","fixture",1000);try{const res=await fetchBridge(f.bridge,"/v1/complete",request());expect(res.status).toBe(504);expect((await res.json()).error.code).toBe("timeout")}finally{await f.close()} + }); + test("structured context overflow stays distinct from quota and generic bad requests",async()=>{ + for(const mode of ["context","generic400"]){const f=await fixture(mode);try{ + const res=await fetchBridge(f.bridge,"/v1/complete",request()),body=await res.json();expect(body.error.code==="context_length_exceeded").toBe(mode==="context");expect(body.error.code).not.toBe("quota_exhausted");if(mode==="context")expect(res.status).toBe(400);expect(f.requests).toHaveLength(1); + }finally{await f.close()}} + }); + test("structured hosted free-tier restriction stays distinct from generic forbidden responses",async()=>{ + for(const mode of ["free-tier","generic403"]){const f=await fixture(mode);try{ + const res=await fetchBridge(f.bridge,"/v1/complete",request()),body=await res.json();expect(body.error.code==="free_tier_restricted").toBe(mode==="free-tier");expect(body.error.code).not.toBe("quota_exhausted");if(mode==="free-tier")expect(res.status).toBe(403);expect(f.requests).toHaveLength(1);expect(JSON.stringify(body)).not.toContain("private policy details"); + }finally{await f.close()}} + }); +}); diff --git a/agent/provider.go b/agent/provider.go index d49b610e..f141956e 100644 --- a/agent/provider.go +++ b/agent/provider.go @@ -10,6 +10,7 @@ package agent import ( "bytes" "context" + "errors" "fmt" "github.com/Autumn-27/artex/locale" "io" @@ -24,6 +25,7 @@ import ( "github.com/Autumn-27/artex/llmnet" "github.com/Autumn-27/artex/llmrec" "github.com/Autumn-27/artex/ollama" + "github.com/Autumn-27/artex/opencodesdk" "github.com/Autumn-27/norma/agentcore" "github.com/Autumn-27/norma/compaction" "github.com/Autumn-27/norma/llm" @@ -185,7 +187,9 @@ func FromEnv() (Config, bool) { // Native Ollama never falls back to a cloud credential. _, explicitKey := runtimeconfig.LLMEnv("API_KEY") if c.APIKey == "" && !explicitKey { - if prov == "opencode-go" { + if prov == opencodesdk.Format { + c.APIKey = os.Getenv("SCOPEWEAVER_OPENCODE_TOKEN") + } else if prov == "opencode-go" { // The wire format resolved to "openai", but the credential is the // OpenCode subscription key — never OPENAI_API_KEY. c.APIKey = os.Getenv("OPENCODE_API_KEY") @@ -232,6 +236,11 @@ func ConfigFrom(provider, model, baseURL, apiKey, proxy string) Config { Stream: true, // Streaming defaults on; callers override from the profile. } switch strings.TrimSpace(provider) { + case opencodesdk.Format: + c.Format = llm.Format(opencodesdk.Format) + if c.BaseURL == "" { + c.BaseURL = opencodesdk.DefaultBaseURL + } case "ollama": c.Format = llm.Format("ollama") c.BaseURL = ollama.NormalizeBaseURL(c.BaseURL) @@ -297,6 +306,8 @@ func isFalsy(s string) bool { // Provider returns the short provider name ("anthropic"/"openai"). func (c Config) Provider() string { switch c.Format { + case llm.Format(opencodesdk.Format): + return opencodesdk.Format case llm.Format("ollama"): return "ollama" case llm.FormatOpenAI: @@ -315,6 +326,23 @@ func (c Config) NewProvider() (llm.Provider, error) { if err != nil { return nil, err } + if c.Provider() == opencodesdk.Format { + if !opencodesdk.ValidModel(c.Model) { + return nil, locale.NewError("OpenCode SDK model must be providerID/modelID") + } + policy := func() bool { + if c.PolicyResolver != nil { + strict, _ := c.PolicyResolver() + return strict + } + return c.LocalLLMOnly + } + p, err := opencodesdk.New(c.BaseURL, c.APIKey, c.Model, opencodesdk.Options{Client: client, LocalOnly: policy, ThinkingType: c.ThinkingType, ReasoningEffort: c.ReasoningEffort}) + if err != nil { + return nil, err + } + return llm.RateLimited(p, llm.RateLimit{PerSecond: c.RatePerSecond, PerMinute: c.RatePerMinute}), nil + } if c.Provider() == "ollama" { native, err := ollama.NewClient(c.BaseURL, c.APIKey, client) if err != nil { @@ -366,6 +394,18 @@ func (c Config) NewProvider() (llm.Provider, error) { return provider, nil } +// IsQuotaExhaustedError trusts structured SDK evidence and preserves other providers' existing message rules. +func IsQuotaExhaustedError(err error) bool { + if err == nil { + return false + } + var bridge *opencodesdk.BridgeError + if errors.As(err, &bridge) { + return bridge.QuotaExhausted() + } + return IsQuotaExhaustedMessage(err.Error()) +} + // IsQuotaExhaustedMessage deliberately recognizes only explicit balance, // billing, credit, or quota-exhaustion signals. Generic 429/rate-limit text, // authentication failures, network errors, and server failures are excluded. @@ -417,6 +457,7 @@ type quotaAwareTransport struct { // carries; its value is the session id read from the request context. Empty // disables it. See Config.SessionHeaderKey. sessionHeaderKey string + preserveStatus bool } func (t quotaAwareTransport) RoundTrip(req *http.Request) (*http.Response, error) { @@ -471,7 +512,7 @@ func (t quotaAwareTransport) RoundTrip(req *http.Request) (*http.Response, error }} } - if resp.StatusCode != http.StatusTooManyRequests { + if resp.StatusCode != http.StatusTooManyRequests || t.preserveStatus { return resp, nil } body, readErr := io.ReadAll(resp.Body) @@ -509,6 +550,9 @@ func requestBodySnapshot(req *http.Request) string { // Ready allows keyless native Ollama and explicit local compatible servers. func (c Config) Ready() bool { + if c.Provider() == opencodesdk.Format { + return strings.TrimSpace(c.APIKey) != "" + } return c.APIKey != "" || c.Provider() == "ollama" || llmnet.IsLocalEndpoint(c.BaseURL) } @@ -516,7 +560,13 @@ func (c Config) Ready() bool { func (c Config) HTTPClient() (*http.Client, error) { var client *http.Client var err error - if c.PolicyResolver != nil { + if c.Provider() == opencodesdk.Format { + if c.Proxy != "" { + return nil, locale.NewError("OpenCode SDK bridge proxy settings are not allowed") + } + // Pin bridge sockets to loopback even if localhost DNS changes; downstream policy is checked separately. + client, err = llmnet.NewClient("", true, nil) + } else if c.PolicyResolver != nil { client, err = llmnet.NewClientWithPolicy(c.Proxy, c.PolicyResolver) } else { client, err = llmnet.NewClient(c.Proxy, c.LocalLLMOnly, c.LocalLLMAllowedCIDRs) @@ -528,7 +578,7 @@ func (c Config) HTTPClient() (*http.Client, error) { if c.officialClaude55() { replay = &claudeReplay{} } - client.Transport = quotaAwareTransport{base: client.Transport, sessionHeaderKey: strings.TrimSpace(c.SessionHeaderKey), claude: replay} + client.Transport = quotaAwareTransport{base: client.Transport, sessionHeaderKey: strings.TrimSpace(c.SessionHeaderKey), claude: replay, preserveStatus: c.Provider() == opencodesdk.Format} return client, nil } diff --git a/agent/provider_sdk_test.go b/agent/provider_sdk_test.go new file mode 100644 index 00000000..0b907782 --- /dev/null +++ b/agent/provider_sdk_test.go @@ -0,0 +1,145 @@ +package agent + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + + "github.com/Autumn-27/artex/opencodesdk" + "github.com/Autumn-27/norma/llm" +) + +func TestConfigFromOpenCodeSDK(t *testing.T) { + c := ConfigFrom(" opencode-sdk ", "opencode/test/model", "", "bridge-token", "") + if c.Provider() != opencodesdk.Format || c.Format != llm.Format(opencodesdk.Format) || c.BaseURL != opencodesdk.DefaultBaseURL || c.Model != "opencode/test/model" || !c.Ready() { + t.Fatalf("config=%+v", c) + } + c.APIKey = "" + if c.Ready() { + t.Fatal("SDK must require its bridge token even on loopback") + } +} +func TestFromEnvOpenCodeSDKTokenIsolation(t *testing.T) { + for _, tc := range []struct { + name, modern, dedicated string + explicit bool + want string + }{{"dedicated only", "", "dedicated-token", false, "dedicated-token"}, {"generic override", "generic-token", "dedicated-token", true, "generic-token"}, {"explicit blank suppresses fallback", "", "dedicated-token", true, ""}, {"never cloud fallback", "", "", false, ""}} { + t.Run(tc.name, func(t *testing.T) { + isolateLLMEnvironment(t) + t.Setenv("SCOPEWEAVER_LLM_PROVIDER", opencodesdk.Format) + t.Setenv("SCOPEWEAVER_LLM_MODEL", "opencode/test") + t.Setenv("SCOPEWEAVER_OPENCODE_TOKEN", tc.dedicated) + t.Setenv("OPENAI_API_KEY", "cloud-openai") + t.Setenv("OPENCODE_API_KEY", "cloud-go") + if tc.explicit { + t.Setenv("SCOPEWEAVER_LLM_API_KEY", tc.modern) + } + cfg, ok := FromEnv() + if cfg.APIKey != tc.want || ok != (tc.want != "") { + t.Fatalf("key=%q ready=%v want=%q", cfg.APIKey, ok, tc.want) + } + }) + } +} +func TestOpenCodeSDKPolicyAndProxy(t *testing.T) { + c := ConfigFrom(opencodesdk.Format, "opencode/test", "", "bridge-token", "http://127.0.0.1:9999") + if _, err := c.NewProvider(); err == nil { + t.Fatal("SDK accepted bridge proxy") + } + c.Proxy = "" + var strict atomic.Bool + strict.Store(true) + c.PolicyResolver = func() (bool, []string) { return strict.Load(), nil } + if _, err := c.NewProvider(); err == nil || !strings.Contains(err.Error(), "local_llm_only") { + t.Fatalf("policy error=%v", err) + } + strict.Store(false) + p, err := c.NewProvider() + if err != nil { + t.Fatal(err) + } + strict.Store(true) + if _, _, _, err = p.Complete(t.Context(), llm.CompletionRequest{}); err == nil || !strings.Contains(err.Error(), "local_llm_only") { + t.Fatalf("cached provider ignored policy change: %v", err) + } +} + +func TestSDKQuotaClassificationUsesStructuredEvidence(t *testing.T) { + cases := []struct { + name string + status int + code, message string + quota bool + }{ + {"genuine quota", 402, "quota_exhausted", "Account allowance unavailable", true}, + {"generic payment failure", 402, "sdk_failure", "quota exceeded", false}, + {"transient rate limit", 429, "rate_limit", "quota exceeded RPM", false}, + {"untrusted 429 quota code", 429, "quota_exhausted", "quota exhausted", false}, + {"auth", 401, "quota_exhausted", "quota exhausted", false}, + {"forbidden", 403, "quota_exhausted", "quota exhausted", false}, + {"server failure", 503, "quota_exhausted", "quota exhausted", false}, + } + for _, stream := range []bool{false, true} { + for _, tc := range cases { + if stream && tc.status != 402 { + continue + } + t.Run(fmt.Sprintf("%v/%s", stream, tc.name), func(t *testing.T) { + b := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v1/capabilities" { + fmt.Fprint(w, `{"protocol":"scopeweaver-opencode-sdk","version":1,"runtime":"native","sdk_version":"2.1.4","tool_execution":"capture-only","streaming":true}`) + return + } + if r.Method == "DELETE" { + return + } + if stream { + w.Header().Set("Content-Type", "application/x-ndjson") + _ = json.NewEncoder(w).Encode(map[string]any{"type": "error", "code": tc.code, "message": tc.message}) + } else { + w.WriteHeader(tc.status) + _ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"code": tc.code, "message": tc.message}}) + } + })) + defer b.Close() + c := ConfigFrom(opencodesdk.Format, "opencode/test", b.URL, "bridge-token", "") + p, err := c.NewProvider() + if err != nil { + t.Fatal(err) + } + var callErr error + if stream { + for _, e := range p.Stream(t.Context(), llm.CompletionRequest{MaxTokens: 64}) { + if e != nil { + callErr = e + } + } + } else { + _, _, _, callErr = p.Complete(t.Context(), llm.CompletionRequest{MaxTokens: 64}) + } + if callErr == nil || IsQuotaExhaustedError(callErr) != tc.quota { + t.Fatalf("quota=%v error=%v want=%v", IsQuotaExhaustedError(callErr), callErr, tc.quota) + } + var bridge *opencodesdk.BridgeError + if !errors.As(callErr, &bridge) { + t.Fatalf("lost structured bridge error: %v", callErr) + } + if !stream && bridge.Status != tc.status { + t.Fatalf("transport rewrote status %d to %d", tc.status, bridge.Status) + } + }) + } + } + for _, model := range []string{"", "model", "p/", "/m", "p//m", "p/m m"} { + c := ConfigFrom(opencodesdk.Format, model, "", "token", "") + if _, err := c.NewProvider(); err == nil { + t.Fatalf("configured invalid SDK model %q", model) + } + } +} diff --git a/db/config.go b/db/config.go index eb979cd9..0d54320f 100644 --- a/db/config.go +++ b/db/config.go @@ -6,6 +6,7 @@ import ( "encoding/json" "github.com/Autumn-27/artex/llmnet" "github.com/Autumn-27/artex/locale" + "github.com/Autumn-27/artex/opencodesdk" "time" ) @@ -152,6 +153,9 @@ ORDER BY is_default DESC, priority DESC, id ASC`) if err := scanProfile(rows, &p.APIKey, &p); err != nil { return nil, err } + if p.Format == opencodesdk.Format && p.APIKey == "" { + continue + } if p.APIKey != "" || p.Format == "ollama" || llmnet.IsLocalEndpoint(p.BaseURL) { out = append(out, &p) } @@ -161,7 +165,13 @@ ORDER BY is_default DESC, priority DESC, id ASC`) // SaveProfile inserts (id==0) or updates a profile. Empty apiKey on update keeps existing. func (d *DB) SaveProfile(p *LLMProfile) (int64, error) { + if p.Format == opencodesdk.Format && !opencodesdk.ValidModel(p.Model) { + return 0, locale.NewError("OpenCode SDK model must be providerID/modelID") + } hint := p.APIKeyHint + if p.APIKey != "" { + hint = "" + } if len(p.APIKey) >= 4 { hint = "…" + p.APIKey[len(p.APIKey)-4:] } @@ -175,10 +185,27 @@ VALUES ($1,$2,NULLIF($3,''),NULLIF($4,''),$5,NULLIF($6,''),NULLIF($7,''),$8,$9,$ return id, err } if p.APIKey == "" { - _, err := d.Exec(`UPDATE llm_profiles SET name=$1,format=$2,api_key=CASE WHEN $2='ollama' AND format<>'ollama' THEN NULL ELSE api_key END,api_key_hint=CASE WHEN $2='ollama' AND format<>'ollama' THEN NULL ELSE api_key_hint END,base_url=NULLIF($3,''),proxy=NULLIF($4,''),model=$5,rate_per_second=$6,rate_per_minute=$7,context_window_k=$8,reasoning_effort=$9,priority=$10,pool_exclude=$11,thinking_type=$12,streaming=$13,max_tokens=$14,max_tokens_field=$15,session_header_key=$16,retry_connect_attempts=$17,retry_connect_interval_ms=$18,retry_empty_attempts=$19,retry_empty_interval_ms=$20,retry_stream_attempts=$21,retry_stream_interval_ms=$22,keep_alive=$23,llm_max_concurrent=$24 WHERE id=$25`, + // Lock the credential scope and update key/hint together; concurrent edits cannot reuse a token from another bridge. + tx, err := d.Begin() + if err != nil { + return p.ID, err + } + defer tx.Rollback() + var oldFormat, oldURL string + if err = tx.QueryRow(`SELECT format,COALESCE(base_url,'') FROM llm_profiles WHERE id=$1 FOR UPDATE`, p.ID).Scan(&oldFormat, &oldURL); err != nil { + if err == sql.ErrNoRows { + return p.ID, nil + } + return p.ID, err + } + clearSDK := (oldFormat == opencodesdk.Format || p.Format == opencodesdk.Format) && (oldFormat != p.Format || !opencodesdk.SameEndpoint(oldURL, p.BaseURL)) + _, err = tx.Exec(`UPDATE llm_profiles SET name=$1,format=$2,api_key=CASE WHEN $26 OR ($2='ollama' AND format<>'ollama') THEN NULL ELSE api_key END,api_key_hint=CASE WHEN $26 OR ($2='ollama' AND format<>'ollama') THEN NULL ELSE api_key_hint END,base_url=NULLIF($3,''),proxy=NULLIF($4,''),model=$5,rate_per_second=$6,rate_per_minute=$7,context_window_k=$8,reasoning_effort=$9,priority=$10,pool_exclude=$11,thinking_type=$12,streaming=$13,max_tokens=$14,max_tokens_field=$15,session_header_key=$16,retry_connect_attempts=$17,retry_connect_interval_ms=$18,retry_empty_attempts=$19,retry_empty_interval_ms=$20,retry_stream_attempts=$21,retry_stream_interval_ms=$22,keep_alive=$23,llm_max_concurrent=$24 WHERE id=$25`, p.Name, p.Format, p.BaseURL, p.Proxy, p.Model, p.RatePerSecond, p.RatePerMinute, p.ContextWindowK, p.ReasoningEffort, p.Priority, p.PoolExclude, p.ThinkingType, p.Streaming, p.MaxTokens, p.MaxTokensField, p.SessionHeaderKey, - r.Connect.Attempts, r.Connect.IntervalMS, r.Empty.Attempts, r.Empty.IntervalMS, r.Stream.Attempts, r.Stream.IntervalMS, p.KeepAlive, p.MaxConcurrent, p.ID) - return p.ID, err + r.Connect.Attempts, r.Connect.IntervalMS, r.Empty.Attempts, r.Empty.IntervalMS, r.Stream.Attempts, r.Stream.IntervalMS, p.KeepAlive, p.MaxConcurrent, p.ID, clearSDK) + if err != nil { + return p.ID, err + } + return p.ID, tx.Commit() } _, err := d.Exec(`UPDATE llm_profiles SET name=$1,format=$2,base_url=NULLIF($3,''),proxy=NULLIF($4,''),model=$5,api_key=$6,api_key_hint=$7,rate_per_second=$8,rate_per_minute=$9,context_window_k=$10,reasoning_effort=$11,priority=$12,pool_exclude=$13,thinking_type=$14,streaming=$15,max_tokens=$16,max_tokens_field=$17,session_header_key=$18,retry_connect_attempts=$19,retry_connect_interval_ms=$20,retry_empty_attempts=$21,retry_empty_interval_ms=$22,retry_stream_attempts=$23,retry_stream_interval_ms=$24,keep_alive=$25,llm_max_concurrent=$26 WHERE id=$27`, p.Name, p.Format, p.BaseURL, p.Proxy, p.Model, p.APIKey, hint, p.RatePerSecond, p.RatePerMinute, p.ContextWindowK, p.ReasoningEffort, p.Priority, p.PoolExclude, p.ThinkingType, p.Streaming, p.MaxTokens, p.MaxTokensField, p.SessionHeaderKey, diff --git a/db/config_sdk_test.go b/db/config_sdk_test.go new file mode 100644 index 00000000..c4602f66 --- /dev/null +++ b/db/config_sdk_test.go @@ -0,0 +1,94 @@ +package db + +import ( + "fmt" + "testing" +) + +func TestSDKProfileCredentialTransitions(t *testing.T) { + d, err := Open(testDSN(t)) + if err != nil { + t.Fatal(err) + } + defer d.Close() + cases := []struct{ name, oldFormat, oldURL, newFormat, newURL, key, want string }{ + {"SDK same endpoint", "opencode-sdk", "http://localhost:4097/a", "opencode-sdk", "http://localhost:4097/a/", "", "old-token"}, + {"SDK default port casing", "opencode-sdk", "HTTP://LOCALHOST:80/a/", "opencode-sdk", "http://localhost/a", "", "old-token"}, + {"SDK different path", "opencode-sdk", "http://localhost:4097/a", "opencode-sdk", "http://localhost:4097/b", "", ""}, + {"SDK different host alias", "opencode-sdk", "http://localhost:4097", "opencode-sdk", "http://127.0.0.1:4097", "", ""}, + {"SDK different port", "opencode-sdk", "http://localhost:4097", "opencode-sdk", "http://localhost:4098", "", ""}, + {"SDK to cloud", "opencode-sdk", "http://localhost:4097", "openai", "https://api.openai.com/v1", "", ""}, + {"cloud to SDK", "openai", "https://api.openai.com/v1", "opencode-sdk", "http://localhost:4097", "", ""}, + {"SDK explicit replacement", "opencode-sdk", "http://localhost:4097/a", "opencode-sdk", "http://localhost:4097/b", "new-token", "new-token"}, + {"cloud unchanged behavior", "openai", "https://one.example/v1", "openai", "https://two.example/v1", "", "old-token"}, + } + for i, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + p := &LLMProfile{Name: fmt.Sprintf("sdk-token-transition-%d", i), Format: tc.oldFormat, BaseURL: tc.oldURL, Model: "opencode/test", APIKey: "old-token", Streaming: true} + id, e := d.SaveProfile(p) + if e != nil { + t.Fatal(e) + } + defer d.Exec(`DELETE FROM llm_profiles WHERE id=$1`, id) + p.ID = id + p.Format = tc.newFormat + p.BaseURL = tc.newURL + p.APIKey = tc.key + p.APIKeyHint = "stale-hint" + if _, e = d.SaveProfile(p); e != nil { + t.Fatal(e) + } + got, e := d.ProfileByID(id) + if e != nil { + t.Fatal(e) + } + var savedHint string + if e = d.QueryRow(`SELECT COALESCE(api_key_hint,'') FROM llm_profiles WHERE id=$1`, id).Scan(&savedHint); e != nil { + t.Fatal(e) + } + wantHint := "" + if tc.want != "" { + wantHint = "…" + tc.want[len(tc.want)-4:] + } + if got.APIKey != tc.want || savedHint != wantHint || got.Format != tc.newFormat || got.BaseURL != tc.newURL { + t.Fatalf("saved=%+v want key=%q hint=%q", got, tc.want, wantHint) + } + }) + } +} + +func TestSDKFormatUpgradePreservesExistingProfiles(t *testing.T) { + d, err := Open(testDSN(t)) + if err != nil { + t.Fatal(err) + } + defer d.Close() + old := &LLMProfile{Name: "sdk-upgrade-existing", Format: "openai", Model: "existing-model", BaseURL: "https://existing.example/v1", APIKey: "existing-key"} + id, err := d.SaveProfile(old) + if err != nil { + t.Fatal(err) + } + defer d.Exec(`DELETE FROM llm_profiles WHERE id=$1`, id) + // Recreate the previous release's constraint only inside this disposable test database. + if _, err = d.Exec(`ALTER TABLE llm_profiles DROP CONSTRAINT llm_profiles_format_check; ALTER TABLE llm_profiles ADD CONSTRAINT llm_profiles_format_check CHECK (format IN ('openai','anthropic','openai-responses','ollama'))`); err != nil { + t.Fatal(err) + } + sdk := &LLMProfile{Name: "sdk-upgrade-new", Format: "opencode-sdk", Model: "opencode/test", BaseURL: "http://localhost:4097", APIKey: "sdk-token"} + if _, err = d.SaveProfile(sdk); err == nil { + t.Fatal("previous schema unexpectedly accepted SDK format") + } + upgraded, err := Open(testDSN(t)) + if err != nil { + t.Fatal(err) + } + defer upgraded.Close() + sdkID, err := upgraded.SaveProfile(sdk) + if err != nil { + t.Fatal(err) + } + defer upgraded.Exec(`DELETE FROM llm_profiles WHERE id=$1`, sdkID) + got, err := upgraded.ProfileByID(id) + if err != nil || got.APIKey != old.APIKey || got.Format != old.Format || got.Model != old.Model || got.BaseURL != old.BaseURL { + t.Fatalf("existing profile changed: %+v err=%v", got, err) + } +} diff --git a/db/schema.sql b/db/schema.sql index 5da9c2c7..cc011d38 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -365,7 +365,7 @@ CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS llm_profiles ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, - format TEXT NOT NULL CHECK (format IN ('openai','anthropic','openai-responses','ollama')), + format TEXT NOT NULL CHECK (format IN ('openai','anthropic','openai-responses','ollama','opencode-sdk')), base_url TEXT, proxy TEXT, model TEXT NOT NULL, @@ -422,7 +422,7 @@ ALTER TABLE llm_profiles ADD COLUMN IF NOT EXISTS streaming BOOLEAN NOT NULL -- Idempotent on every startup: drop the old CHECK and recreate with all supported values. ALTER TABLE llm_profiles DROP CONSTRAINT IF EXISTS llm_profiles_format_check; ALTER TABLE llm_profiles ADD CONSTRAINT llm_profiles_format_check - CHECK (format IN ('openai','anthropic','openai-responses','ollama')); + CHECK (format IN ('openai','anthropic','openai-responses','ollama','opencode-sdk')); -- Native Ollama options are additive; cloud profiles retain their existing defaults. ALTER TABLE llm_profiles ADD COLUMN IF NOT EXISTS keep_alive TEXT NOT NULL DEFAULT ''; diff --git a/docs/es/llm-providers.md b/docs/es/llm-providers.md index f236730b..ebae4986 100644 --- a/docs/es/llm-providers.md +++ b/docs/es/llm-providers.md @@ -2,7 +2,9 @@ [English](../llm-providers.md) · [한국어](../ko/llm-providers.md) · [简体中文](../zh-Hans/llm-providers.md) · [繁體中文](../zh-Hant/llm-providers.md) · [日本語](../ja/llm-providers.md) · Español -ScopeWeaver admite cuatro formatos de perfil: **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses** y **Ollama** nativo. Todos conservan URL base personalizada, proxy, cabecera de sesión, reintentos, frecuencia y concurrencia, por lo que siguen funcionando sin cambios proveedores de nube, pasarelas y servidores autoalojados compatibles con OpenAI. Ollama es opcional y los proveedores de nube siguen siendo la ruta compatible predeterminada. +ScopeWeaver admite **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses**, **Ollama** nativo y el **puente OpenCode SDK** opcional. Los cuatro formatos anteriores conservan URL base, proxy, cabecera de sesión, reintentos, frecuencia y concurrencia; los proveedores de nube, pasarelas y servidores OpenAI compatibles siguen funcionando. SDK usa credenciales independientes y los límites descritos abajo; no sustituye API/OpenCode Go. + +La plantilla SDK tampoco hace solicitudes; el descubrimiento requiere pulsar **Cargar modelos** explícitamente. Una plantilla rellena el **formulario de un perfil nuevo** sin guardar credenciales, activar perfiles ni modificar los existentes. Las de Ollama también leen modelos instalados del servidor elegido; las de nube no hacen solicitudes. El formato, URL base y modelo siguen editables tras cualquier plantilla. @@ -34,6 +36,55 @@ Introduce nombre de perfil y tu clave API. Aplicar cualquiera de las plantillas Verificado con documentación oficial el **2026-10-03**. La disponibilidad, derechos de cuenta y políticas pueden cambiar. + + +## Puente OpenCode SDK + +`opencode-sdk` es un formato de perfil opcional e independiente, no la API HTTP interna de OpenCode. El entorno Norma de ScopeWeaver conserva conversación, herramientas, aprobaciones, alcance, reintentos y compactación. El puente solo transmite un paso del modelo y captura solicitudes de herramientas del anfitrión. + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | Entorno fijado | Límites de argumentos e historial | +| --- | --- | --- | +| `native` (predeterminado) | CLI, cliente y SDK oficiales **2.1.4** | Conserva JSON original de argumentos, incluidos enteros de 64 bits, en respuestas e historial. Rechaza números inseguros fuera de los argumentos originales autoritativos. | +| `legacy` | SDK y CLI **1.19.4** | JSON estructuralmente válido y representable con seguridad; enteros de valor absoluto no superior a **9,007,199,254,740,991**. Puede normalizar espacios y escapes HTML/Unicode equivalentes, sin conservar bytes originales. Rechaza enteros inseguros e historial opaco/firmado de razonamiento. Usa `native` para fidelidad de argumentos originales. | + +El modo nativo usa el CLI auténtico fijado con `serve --stdio` y `run --server --session`; el cliente oficial gestiona sesiones, eventos e interrupción. Cada llamada controla home/configuración/datos temporales privados y su base de datos. Conserva las definiciones e implementaciones nativas reales, con permisos ASK y sin `--auto`; un hook público `execute.before` bloquea todas las herramientas ajenas al anfitrión antes de ejecutarse. Solo devuelve las capturas a Norma para ejecución y aprobación. Conserva el contexto generado de modelo/entorno/herramientas junto con system e historial del anfitrión. Rechaza fuentes de configuración de sistema/MDM no controladas sin leerlas ni modificarlas; consulta la guía del adaptador. + +Los proveedores personalizados heredados usan un mapeo explícito compatible con OpenAI. **Ninguno de los dos entornos admite Windows**, porque no está implementada la limpieza necesaria del árbol POSIX. La verificación real previa cubrió **macOS arm64**. Linux CI está configurada, pero su ejecución alojada sigue sin verificarse. No se afirman resultados en otras plataformas; versiones SDK no probadas requieren verificación independiente. + +Sigue el [inicio del adaptador](../../adapters/opencode-sdk/README.es.md) con **Bun 1.3.14**. El puente tiene configuración propia y no importa credenciales guardadas de OpenCode ni de nube. Define un `SCOPEWEAVER_OPENCODE_TOKEN` independiente de al menos **32 caracteres no blancos** y úsalo en **LLM → Nuevo → Plantilla de configuración → Puente OpenCode SDK**. No pegues una clave API de OpenCode Go/Console. + +```sh +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +Conserva el token para el campo **Token del puente**. El entorno predeterminado es `native`; define `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` antes del inicio para elegir el antiguo. Escucha en `127.0.0.1:4097`. Para proveedores explícitos, usa un JSON dedicado mediante `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG`, siguiendo el esquema del adaptador, no un archivo de autenticación del usuario. Conserva las dependencias opcionales de plataforma. + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` admite **0–86,400,000 ms**, con **0** predeterminado bajo control de cancelación del anfitrión. La solicitud tiene un máximo de **16 MiB** y el exceso falla explícitamente. Los demás límites de salida y limpieza están en la guía del adaptador. + +| Campo | Valor | +| --- | --- | +| Formato | `opencode-sdk` | +| URL del puente | `http://127.0.0.1:4097` | +| Modelo | `providerID/modelID`, mediante **Cargar modelos** o entrada explícita | +| Token del puente | `SCOPEWEAVER_OPENCODE_TOKEN`, no una clave API de nube | +| Streaming | Activado, con el entorno fijado seleccionado | +| Razonamiento / esfuerzo | Omitidos por la plantilla; usa solo valores admitidos por el modelo | +| Límite de salida | `0` usa el valor existente de la ruta del anfitrión; el puente aplica el límite declarado menor del modelo | +| Proxy / cabecera de sesión personalizada | No admitidos; la plantilla borra ambos | + +La plantilla borra claves de nube sin guardar, deja el modelo vacío y **no descubre, infiere, guarda ni activa**. Introduce el token y pulsa **Cargar modelos** para consultar el catálogo sin inferencia. Guarda y activa por separado cuando esté listo. **Probar conexión** solo hace una solicitud al modelo al pulsarlo; descubrir metadatos no demuestra acceso de inferencia. + +**Verificación gratuita anterior — 2026-10-25 KST:** si figura en la lista, selecciona `opencode/deepseek-v4.1-flash-free`. La ejecución anterior sin iniciar sesión en macOS arm64, con CLI auténtico 2.1.4, superó conexión de interfaz, escritura Bash del anfitrión tras aprobación ASK manual, archivo con `FREE_OK` exacto más salto de línea y respuesta final. Hubo **3 POST de inferencia**, salida máxima **1,024**, reintentos desactivados, sin modelo de pago alternativo ni ejecución nativa observada. Solo demuestra un pequeño flujo aislado de esa fecha, no una repetición actual, disponibilidad permanente ni uso ilimitado. Verifica disponibilidad, elegibilidad y límites actuales. El acceso gratuito alojado heredado sigue sin verificarse. + +Al editar, un token vacío conserva el guardado solo en el mismo endpoint SDK. Cambiar URL borra el token introducido; cambiar endpoint o formato con token vacío no reutiliza credenciales. `localhost`, `127.0.0.1` y `::1` son ámbitos distintos. ID y límites de salida proceden de metadatos actuales. El razonamiento depende del proveedor/modelo y falla explícitamente si no es compatible. + +**Red y cuentas:** el puente loopback debe compartir el espacio de nombres de red con ScopeWeaver; localhost dentro de un contenedor es el propio contenedor. El destino puede ser nube, por lo que el modo **LLM solo local** rechaza SDK. El puente no admite proxies; configura la conectividad del proveedor en el puente. Un ID gratuito no demuestra derechos de suscripción, continuidad ni uso ilimitado; revisa términos y límites. No selecciona alternativas de pago ni importa cuentas. La política de failover configurada por el anfitrión es independiente. Los perfiles API/OpenCode Go existentes siguen separados. + +**Preparar una degradación:** la restricción de formato `CHECK` de la base de datos añade `opencode-sdk`. Un binario anterior no arranca mientras queden perfiles SDK. Respalda la base y usa la versión actual para convertirlos a un formato antiguo admitido o eliminarlos antes de degradar. La verificación previa usó bases desechables, sin migrar producción. + ## OpenCode Go (pasarela de suscripción opencode) Abre **LLM → Nuevo → Plantilla de configuración** y elige **OpenCode Go · glm-5.3-flash**. diff --git a/docs/ja/llm-providers.md b/docs/ja/llm-providers.md index 705fffde..4fb81bfc 100644 --- a/docs/ja/llm-providers.md +++ b/docs/ja/llm-providers.md @@ -5,7 +5,9 @@ -ScopeWeaver は **Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses**、ネイティブ **Ollama** の 4 つのプロファイル形式に対応します。各形式は独自のベース URL、プロキシ、セッションヘッダー、再試行、レート、同時実行設定を保持し、クラウド、ゲートウェイ、セルフホストの OpenAI 互換サーバーはそのまま動作します。Ollama は任意であり、クラウドは引き続き既定と互換性のある経路です。 +ScopeWeaver は **Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses**、ネイティブ **Ollama** と任意の **OpenCode SDK ブリッジ**に対応します。元の 4 形式は独自ベース URL、プロキシ、セッションヘッダー、再試行、レート、同時実行設定を保持し、クラウド、ゲートウェイ、セルフホスト OpenAI 互換サーバーの動作は変わりません。SDK は独立した認証情報と下記の制限を使い、API/OpenCode Go を置き換えません。 + +SDK テンプレートも要求を送信しません。モデル探索には明示的な**モデル読み込み**操作が必要です。 テンプレートは、認証情報を保存したり、プロファイルを有効化したり、既存のものを変更したりせず、**新規プロファイルのフォーム**を設定します。Ollama のテンプレートは選択したサーバーのインストール済みモデルも読みますが、クラウドのテンプレートは要求を送りません。適用後も形式、ベース URL、モデルを編集できます。 @@ -39,6 +41,55 @@ GLM-5.3 はテキストを受け付け、コンテキストは 1M トークン 公式資料の確認日: **2026-10-03**。プロバイダーの可用性、アカウントの権利、方針は変わる場合があります。 + + +## OpenCode SDK ブリッジ + +`opencode-sdk` は独立した任意のプロファイル形式であり、OpenCode 内蔵 HTTP API ではありません。ScopeWeaver の Norma が対話、ツール、承認、スコープ、再試行、圧縮を管理します。ブリッジはモデルの 1 ステップとホストツール要求の収集のみを処理します。 + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定ランタイム | 引数・履歴の境界 | +| --- | --- | --- | +| `native`(既定) | 公式 CLI/クライアント/SDK **2.1.4** | 応答とホスト履歴の生ツール引数 JSON を 64 ビット整数も含め保持。正本の生引数以外で危険な数値は拒否。 | +| `legacy` | SDK/CLI **1.19.4** | 正しく安全に表現できる JSON、絶対値 **9,007,199,254,740,991** 以下の整数のみ。空白や同値 HTML/Unicode エスケープは正規化され、生のバイトは保持しません。危険な整数と不透明/署名付き思考履歴は拒否。生引数の忠実性が必要なら `native` を使用。 | + +ネイティブは固定した本物の CLI `serve --stdio`、`run --server --session` と公式クライアントでセッション、イベント、中断を処理します。呼び出しごとに私有の一時 home/設定/データと DB を所有します。実際の組み込みツール定義・実装は残しますが、権限は ASK、`--auto` は使わず、公開 `execute.before` が非ホストツールをすべて実行前に拒否します。ホスト収集要求だけを Norma に戻して実行・承認します。OpenCode のモデル/環境/ツール文脈をホスト system と履歴に加えて保持します。管理できないシステム/MDM 設定がある場合の起動拒否は、読み取り・変更を行わずに適用します。詳細はアダプターガイドにあります。 + +旧版のカスタムプロバイダーは明示的な OpenAI 互換マッピングを使います。必要な POSIX プロセスツリー終了処理が未実装のため、**両ランタイムとも Windows 非対応**です。過去の実ランタイムテスト範囲は **macOS arm64**。Linux CI は設定済みですが、ホステッド結果は未検証です。他のプラットフォームや未テスト SDK バージョンの動作は保証しません。 + +[アダプターの起動手順](../../adapters/opencode-sdk/README.ja.md)に従い **Bun 1.3.14** を使います。ブリッジは独自設定を持ち、保存済み OpenCode/クラウド認証情報を取り込みません。空白以外の文字 **32 文字以上**の独立した `SCOPEWEAVER_OPENCODE_TOKEN` を設定し、**LLM → 新規 → 設定テンプレート → OpenCode SDK ブリッジ**に入力します。OpenCode Go/Console API キーを貼り付けないでください。 + +```sh +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +トークンをプロファイルの**ブリッジトークン**欄用に保持します。既定は `native`、起動前の `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` で旧版を選択します。待受は `127.0.0.1:4097`。明示的プロバイダーには `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` の専用 JSON を使います。ユーザーの認証ファイルではなく、アダプター記載の形式を使用してください。オプションのプラットフォーム依存を省略しません。 + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` は **0–86,400,000 ミリ秒**、既定 **0** はホストによるキャンセルに委ねます。要求本文は **16 MiB** が上限で、超過は明示的に失敗します。他の出力・終了処理制限はアダプターガイドを参照してください。 + +| 項目 | 値 | +| --- | --- | +| 形式 | `opencode-sdk` | +| ブリッジ URL | `http://127.0.0.1:4097` | +| モデル | **モデル読み込み**で選択、または正確な `providerID/modelID` | +| ブリッジトークン | ブリッジの `SCOPEWEAVER_OPENCODE_TOKEN`。クラウド API キーではありません | +| ストリーミング | 有効。選択した固定ランタイムを使用 | +| 思考/推論強度 | テンプレートでは省略。モデル対応値のみ指定 | +| 出力上限 | `0` は従来のホスト経路の既定値。モデルの宣言する小さい上限で制限 | +| プロキシ/独自セッションヘッダー | 非対応。テンプレートで両方を消去 | + +テンプレートは未保存のクラウドキーを消し、モデルを空にして、**探索・推論・保存・有効化を行いません**。トークン入力後、明示的に**モデル読み込み**を押すと推論せずに一覧を取得します。保存と有効化は別操作です。**接続テスト**はクリック時だけモデル要求を実行し、メタデータの取得成功は推論可能という証明ではありません。 + +**過去の無料モデル検証 — 2026-10-25 KST:** 一覧にある場合、`opencode/deepseek-v4.1-flash-free` を選べます。当時の未ログイン macOS arm64 実行では本物の CLI 2.1.4 を使い、UI 接続テスト、手動 ASK 承認後のホスト Bash 書き込み、正確な `FREE_OK` と改行のファイル内容、最終回答が成功しました。推論 POST **3 回**、出力上限 **1,024**、再試行無効、有料代替なしで、ネイティブツール実行は観測していません。これは当時の小さな隔離ワークフローだけの証拠であり、今回の再実行、永続提供、無制限利用を示しません。現在の提供状況、資格、制限を確認してください。旧版のホステッド無料アクセスは未検証です。 + +編集中に空トークンで保存値を保持できるのは同じ SDK URL のみです。URL 変更で入力トークンを消し、宛先・形式変更後の空欄では保存認証情報を流用しません。`localhost`、`127.0.0.1`、`::1` は別スコープです。モデル ID/出力上限は現在のメタデータを使います。推論設定はモデル・プロバイダー次第で、未対応なら明示的に失敗します。 + +**ネットワークとアカウント:** ブリッジと ScopeWeaver は同じネットワーク名前空間で動かします。コンテナー内 localhost はコンテナー自身です。下流はクラウドの場合があるため、**ローカル専用 LLM** モードは SDK を拒否します。ブリッジプロキシは非対応で、下流接続はブリッジ側で設定します。無料モデル ID は購読資格、継続提供、無制限利用を保証しません。規約と現在の制限を確認してください。ブリッジは有料代替やアカウント取り込みを行いません。ホストの設定済みフェイルオーバーは別管理です。既存 API/OpenCode Go プロファイルは独立しています。 + +**ダウングレード準備:** DB のプロファイル形式 `CHECK` 制約に `opencode-sdk` を追加します。SDK プロファイルが残ると旧バイナリは起動できません。DB をバックアップし、現バージョンで旧対応形式に変換するか SDK プロファイルを削除してから戻します。過去の検証は使い捨て DB のみで、稼働 DB は移行していません。 + ## OpenCode Go(opencode のサブスクリプションゲートウェイ) **LLM → 新規 → 設定テンプレート**で **OpenCode Go · glm-5.3-flash** を選択します。 diff --git a/docs/ko/llm-providers.md b/docs/ko/llm-providers.md index 15ebdf71..938a30c5 100644 --- a/docs/ko/llm-providers.md +++ b/docs/ko/llm-providers.md @@ -2,9 +2,11 @@ [English](../llm-providers.md) · 한국어 · [简体中文](../zh-Hans/llm-providers.md) · [繁體中文](../zh-Hant/llm-providers.md) · [日本語](../ja/llm-providers.md) · [Español](../es/llm-providers.md) -ScopeWeaver는 **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses**, 기본 **Ollama**의 네 가지 프로필 형식을 지원합니다. 모든 형식에서 사용자 지정 기본 URL, 프록시, 세션 헤더, 재시도, 속도 제한, 동시 요청 설정을 그대로 사용할 수 있으므로 클라우드 제공자, 게이트웨이, 직접 운영하는 OpenAI 호환 서버도 이전처럼 동작합니다. Ollama는 선택 사항이며 클라우드 제공자는 기본적으로 계속 호환됩니다. +ScopeWeaver의 기존 네 가지 프로필 형식은 **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses**, 기본 **Ollama**입니다. 모든 형식에서 사용자 지정 기본 URL, 프록시, 세션 헤더, 재시도, 속도 제한, 동시 요청 설정을 그대로 사용할 수 있으므로 클라우드 제공자, 게이트웨이, 직접 운영하는 OpenAI 호환 서버도 이전처럼 동작합니다. Ollama는 선택 사항이며 클라우드 제공자는 기본적으로 계속 호환됩니다. -템플릿은 자격 증명을 저장하거나 프로필을 활성화하거나 기존 프로필을 변경하지 않고 **새 프로필의 양식을 채웁니다**. Ollama 템플릿은 선택한 서버에 설치된 모델도 조회하며, 클라우드 템플릿은 요청을 보내지 않습니다. 템플릿을 적용한 뒤에도 형식, 기본 URL, 모델은 수정할 수 있습니다. +선택형 **OpenCode SDK 브리지**는 별도 자격 증명과 아래의 제약을 사용하는 프로필 형식을 추가합니다. 기존 API나 OpenCode Go 게이트웨이 경로를 대체하지 않습니다. + +템플릿은 자격 증명을 저장하거나 프로필을 활성화하거나 기존 프로필을 변경하지 않고 **새 프로필의 양식을 채웁니다**. Ollama 템플릿은 선택한 서버에 설치된 모델도 조회하지만, 클라우드와 SDK 템플릿은 요청을 보내지 않습니다. SDK 모델 조회는 **모델 불러오기**를 눌러야 실행됩니다. 어느 템플릿을 선택하든 형식, 기본 URL, 모델을 계속 수정할 수 있습니다. ## Z.ai GLM-5.3 @@ -34,6 +36,57 @@ GLM-5.3은 텍스트 입력과 100만 토큰의 문맥 창을 지원하며 추 공식 문서 확인일: **2026-10-03**. 모델 제공 여부, 계정 권한 및 정책은 바뀔 수 있습니다. + + +## OpenCode SDK 브리지 + +같은 `opencode-sdk` 프로필 형식으로 고정된 두 런타임을 연결하는 별도 백엔드입니다. 호스트 도구 실행과 승인 검사는 ScopeWeaver의 기존 Norma 런타임이 계속 관리합니다. + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | 고정 런타임 | 지원하는 입력 범위 | +| --- | --- | --- | +| `native`(기본값) | 공식 CLI·`@opencode/client` 2.1.4 | 응답과 호스트 기록에서 64비트 정수를 포함한 도구 인자의 원본 JSON을 보존합니다. 원본 도구 인자 밖의 안전하지 않은 숫자는 거부합니다. | +| `legacy` | SDK·CLI 1.19.4 | 구조가 유효하고 안전하게 표현할 수 있는 JSON 기록과 JavaScript 안전 정수를 허용합니다(절댓값 9,007,199,254,740,991 이하). 무해한 공백이나 같은 값을 나타내는 HTML·Unicode 이스케이프는 정규화될 수 있으며 원본 표기 바이트는 보존하지 않습니다. 안전하지 않은 정수와 불투명하거나 서명된 사고 기록은 명시적으로 거부합니다. 도구 인자 원본을 손실 없이 보존해야 하면 `native`를 사용하세요. | + +`native`는 고정한 실제 CLI의 `serve --stdio`와 `run --server --session`을 실행하며, 공식 클라이언트가 모델 조회·이벤트·중단을 관찰하고 제어합니다. 호출마다 비공개 임시 CLI home/config/data 경로와 데이터베이스 저장소를 소유합니다. 실제 기본 도구 정의와 원래 구현은 등록된 상태로 유지합니다. 기본 도구 권한은 ASK이며, 별도의 공개 `execute.before` 가드가 모든 비호스트 도구의 실행을 사전에 거부합니다. 수집한 호스트 도구 요청은 Norma가 실행·승인하도록 반환합니다. 호스트의 시스템 지침과 기록에 OpenCode가 생성한 모델·환경·도구 문맥도 유지됩니다. 관리되지 않는 시스템·MDM 설정이 있으면 실행하지 않는 검사는 어댑터 안내를 참고하세요. 해당 설정을 읽거나 변경하지 않습니다. + +`legacy`의 사용자 지정 제공자는 어댑터 안내의 명시적인 OpenAI 호환 매핑을 사용합니다. 필요한 POSIX 프로세스 트리 정리 경계가 구현되지 않아 **두 런타임 모두 Windows에서 지원하지 않습니다.** 실제 런타임 테스트는 **macOS arm64**에서 실행했습니다. Linux CI는 구성했지만 아직 호스팅된 실행으로 Linux 런타임 결과를 확인하지 않았습니다. 그 밖의 플랫폼도 검증했다고 주장하지 않습니다. 테스트하지 않은 SDK 버전은 별도 검증이 필요합니다. + +[어댑터 설정 안내](../../adapters/opencode-sdk/README.ko.md)에 따라 Bun 브리지를 실행하세요. 브리지는 자체 제공자 설정을 사용하며, 사용자가 저장한 OpenCode 또는 클라우드 제공자 자격 증명을 가져오지 않습니다. 브리지에 별도의 `SCOPEWEAVER_OPENCODE_TOKEN`을 설정한 뒤(공백 없는 32자 이상), **LLM → 새로 만들기 → 설정 템플릿 → OpenCode SDK · 브리지**를 선택하세요. + +검증한 **Bun 1.3.14**를 사용하세요. 이후 Bun 버전은 아직 검증하지 않았습니다. 저장소 루트에서 실행하세요. + +```bash +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +생성한 토큰은 프로필의 **브리지 토큰** 필드에도 입력해야 합니다. 기본 런타임은 `native`(SDK 2.1.4)입니다. SDK 1.19.4를 선택하려면 브리지 실행 전에 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy`를 설정하세요. 기본 수신 주소는 `127.0.0.1:4097`입니다. 제공자를 직접 설정하려면 전용 JSON 파일을 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG`로 지정하세요. 사용자 인증 파일을 지정하지 말고 어댑터 안내의 스키마를 따르세요. + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS`는 0~86,400,000ms를 받습니다. 기본값 `0`에서는 호스트의 취소가 실행 시간을 제어합니다. 요청 본문은 **16 MiB**로 제한하며 초과 입력은 오류로 알립니다. 나머지 출력·정리 한도는 어댑터 안내를 참고하세요. + +| 필드 | 값 | +| --- | --- | +| 형식 | `opencode-sdk` | +| 브리지 URL | `http://127.0.0.1:4097` | +| 모델 | **모델 불러오기**로 선택하거나 직접 입력하는 `providerID/modelID` | +| 브리지 토큰 | 클라우드 API 키가 아닌 브리지의 `SCOPEWEAVER_OPENCODE_TOKEN` | +| 스트리밍 | 켜짐, 선택한 고정 런타임의 스트리밍 | +| 추론 / 추론 강도 | 템플릿에서는 생략, 선택한 모델이 지원하는 값만 설정 | +| 출력 한도 | `0`은 기존 호스트 호출 경로의 기본값을 사용하며, 브리지가 네이티브 모델에 선언된 출력 한도 이내로 제한 | +| 프록시 / 사용자 지정 세션 헤더 | 지원하지 않으며 템플릿이 두 값을 모두 지움 | + +템플릿은 저장하지 않은 클라우드 키를 지우고 모델을 빈칸으로 두며, **모델 조회·추론·저장·활성화 요청을 보내지 않습니다**. 브리지 토큰을 입력하고 **모델 불러오기**를 눌러야 추론 없이 목록을 조회합니다. 준비되면 저장하고 별도로 활성화하세요. **연결 테스트**는 눌렀을 때만 모델을 호출합니다. 메타데이터를 조회할 수 있다고 추론 접근 권한까지 확인되는 것은 아닙니다. + +**이전 검증의 무료 모델 시작점 — 2026-10-25 KST:** **모델 불러오기**에 표시되면 `opencode/deepseek-v4.1-flash-free`를 선택할 수 있습니다. macOS arm64에서 로그인하지 않은 상태로 실제 native CLI 2.1.4를 사용해 UI 연결 테스트, 수동 ASK 승인 후 호스트 Bash 쓰기, 정확한 `FREE_OK` 파일 내용과 줄바꿈, 최종 어시스턴트 응답을 확인했습니다. 출력 한도 1,024, 재시도 끄기 설정으로 추론 POST 3회를 사용했으며 유료 대체 모델이나 기본 도구 실행은 관찰하지 않았습니다. 작은 격리 작업 하나의 검증이며, 현재 제공 여부·무료 이용 자격·한도는 별도로 적용됩니다. + +수정할 때 토큰을 비우면 같은 SDK 엔드포인트에 저장된 값만 유지합니다. URL을 수정하면 입력한 토큰을 지웁니다. 엔드포인트나 형식을 바꾼 뒤 토큰을 비우면 저장된 자격 증명을 재사용하지 않습니다. `localhost`, `127.0.0.1`, `::1` 같은 호스트 별칭은 서로 다른 토큰 범위입니다. 모델 ID와 출력 한도는 현재 네이티브 모델의 메타데이터를 따릅니다. 추론 설정은 모델·제공자의 지원 여부에 따라 제공자에서 실패할 수 있으며, 오류는 명시적으로 알립니다. + +**네트워크와 계정 제약:** 루프백 전용 브리지는 ScopeWeaver와 같은 네트워크 네임스페이스에서 실행하세요. Docker 안의 localhost는 해당 컨테이너를 가리킵니다. 하위 제공자는 클라우드 서비스일 수 있습니다. 따라서 SDK 프로필은 **로컬 전용 LLM** 모드에서 거부됩니다. 브리지 프록시는 지원하지 않으며, 하위 제공자의 연결은 브리지에서 구성하세요. 무료 모델 ID는 현재 이용 가능하다는 뜻일 뿐 구독 사용 권한, 영구 제공, 무제한 이용을 보장하지 않습니다. 제공자의 약관과 현재 한도를 확인하세요. 브리지는 유료 대체 모델을 선택하거나 사용자 계정을 가져오지 않으며, 호스트에 설정한 장애 조치 정책은 별도로 유지됩니다. 기존 API와 OpenCode Go 프로필은 별도로 유지됩니다. + +**다운그레이드 준비:** 이 변경은 프로필 형식의 데이터베이스 `CHECK` 제약에 `opencode-sdk`를 추가합니다. SDK 프로필이 남아 있으면 이전 바이너리는 시작할 수 없습니다. 다운그레이드 전에 데이터베이스를 백업하고 현재 버전에서 이전 버전이 지원하는 프로필 형식으로 변환하거나 SDK 프로필을 제거할 준비를 하세요. 검증은 임시 데이터베이스에서 수행했으며 여기서 실제 운영 데이터베이스를 마이그레이션하지 않았습니다. + ## OpenCode Go(opencode 구독 게이트웨이) **LLM → 새로 만들기 → 설정 템플릿**에서 **OpenCode Go · glm-5.3-flash**를 고르세요. diff --git a/docs/llm-providers.md b/docs/llm-providers.md index 20d20866..07fdc206 100644 --- a/docs/llm-providers.md +++ b/docs/llm-providers.md @@ -2,9 +2,11 @@ English · [한국어](ko/llm-providers.md) · [简体中文](zh-Hans/llm-providers.md) · [繁體中文](zh-Hant/llm-providers.md) · [日本語](ja/llm-providers.md) · [Español](es/llm-providers.md) -ScopeWeaver supports four profile formats: **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses** and native **Ollama**. Every format keeps its custom base URL, proxy, session header, retry, rate and concurrency settings, so cloud providers, gateways and self-hosted OpenAI-compatible servers keep working unchanged. Ollama is optional, and cloud providers remain the default-compatible path. +The four existing ScopeWeaver profile formats are: **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses** and native **Ollama**. Every format keeps its custom base URL, proxy, session header, retry, rate and concurrency settings, so cloud providers, gateways and self-hosted OpenAI-compatible servers keep working unchanged. Ollama is optional, and cloud providers remain the default-compatible path. -A template fills a **new profile's form** without saving credentials, activating a profile, or modifying existing profiles. Ollama templates also read installed models from the selected server; cloud templates make no requests. Format, base URL and model stay editable after any template. +The optional **OpenCode SDK bridge** adds a separate profile format with its own credentials and restrictions described below. It does not replace the existing API or OpenCode Go gateway paths. + +A template fills a **new profile's form** without saving credentials, activating a profile, or modifying existing profiles. Ollama templates also read installed models from the selected server; cloud and SDK templates make no requests. SDK model discovery requires an explicit **Load models** action. Format, base URL and model stay editable after any template. ## Z.ai GLM-5.3 @@ -34,6 +36,57 @@ Enter a profile name and your own API key. Applying either template preserves an Verified against official documentation on **2026-10-03**. Provider availability, account entitlement, and policies may change. + + +## OpenCode SDK bridge + +This separate backend supports two pinned runtimes through the same `opencode-sdk` profile format. ScopeWeaver's existing Norma runtime remains responsible for host tool execution and approval checks. + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | Pinned runtime | Supported input boundary | +| --- | --- | --- | +| `native` (default) | Official CLI and `@opencode/client` 2.1.4 | Preserves raw tool-argument JSON, including 64-bit integers, through responses and host history. Unsafe numeric values outside authoritative raw arguments are rejected. | +| `legacy` | SDK and CLI 1.19.4 | Structurally valid, safely representable JSON history and JavaScript-safe integers (absolute value no greater than 9,007,199,254,740,991). Benign whitespace and equivalent HTML/Unicode escapes may be normalized; original lexical bytes are not preserved. Unsafe integers and opaque/signed thinking history fail explicitly. Use `native` when lossless raw tool arguments are required. | + +`native` starts the genuine pinned CLI through `serve --stdio` and `run --server --session`; the official client handles discovery, event observation and interruption. Each call owns private temporary CLI home/config/data paths and database storage. Genuine native tool definitions and original implementations remain registered: native permissions are ASK, and a separate public `execute.before` guard denies every non-host tool before execution. Host capture requests return to Norma for execution and approvals. OpenCode's generated model/environment/tool context remains alongside the host's system instructions and history. See the adapter guide for the fail-closed unmanaged system/MDM configuration checks; those sources are not read or changed. + +Custom providers for `legacy` use the explicit OpenAI-compatible mapping in the adapter guide. **Both runtimes are unsupported on Windows** because the required POSIX process-tree cleanup boundary is not implemented there. Actual runtime tests were run on **macOS arm64**. Linux CI is configured, but its hosted run has not yet established Linux runtime results; other platforms are not claimed as verified. Untested SDK versions require separate verification. + +Start the Bun bridge using the [adapter setup instructions](../adapters/opencode-sdk/README.md). It has its own provider configuration; it does not import saved user OpenCode or cloud-provider credentials. Set a separate `SCOPEWEAVER_OPENCODE_TOKEN` on the bridge (at least 32 non-whitespace characters), then open **LLM → New → Configuration template → OpenCode SDK · bridge**. + +Use the tested **Bun 1.3.14** runtime; newer Bun versions have not been verified. From the repository root: + +```bash +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +Keep this token available for the profile's **Bridge token** field. The default runtime is `native` (SDK 2.1.4); set `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` before starting the bridge to select SDK 1.19.4. The listener defaults to `127.0.0.1:4097`. For explicit provider settings, use a dedicated JSON file through `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG`; follow the adapter guide's schema rather than pointing it at a saved user auth file. + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` accepts 0–86,400,000 ms; its default `0` leaves duration under host cancellation control. Request bodies are limited to **16 MiB** and oversized input fails explicitly. See the adapter guide for the remaining output and cleanup bounds. + +| Field | Value | +| --- | --- | +| Format | `opencode-sdk` | +| Bridge URL | `http://127.0.0.1:4097` | +| Model | `providerID/modelID`, chosen using **Load models** or entered explicitly | +| Bridge token | The bridge's `SCOPEWEAVER_OPENCODE_TOKEN`, not a cloud API key | +| Streaming | On; streams the selected pinned runtime | +| Thinking / reasoning effort | Omitted by the template; set only values supported by the selected model | +| Output limit | `0` uses the existing host call-path default; the bridge caps requests at the native model's declared output limit | +| Proxy / custom session header | Unsupported; the template clears both | + +The template clears an unsaved cloud key, leaves the model blank and makes **no model, inference, save or activation request**. Enter the bridge token and click **Load models** to read its catalog without inference. Save when ready and activate separately. **Test connection** performs a model request only when clicked; metadata discovery does not establish inference access. + +**Prior verified free-model starting point — 2026-10-25 KST:** choose `opencode/deepseek-v4.1-flash-free` from **Load models** when it is listed. A signed-out macOS arm64 run through genuine native CLI 2.1.4 passed the UI connection test, a host Bash write after manual ASK approval, exact `FREE_OK` file contents plus newline, and the final assistant answer. It used 3 inference POSTs, an output cap of 1,024 and disabled retries, with no paid fallback or observed native built-in execution. This proves one small isolated workflow; current availability, free-tier eligibility and limits still apply. + +When editing, a blank token retains the saved value only for the same SDK endpoint. Changing the URL clears an entered token; changing the endpoint or format with a blank token does not reuse the saved credential. Host aliases such as `localhost`, `127.0.0.1` and `::1` are distinct token scopes. Model IDs and output limits use current native model metadata. Reasoning settings depend on model/provider support and can fail at the provider; errors remain explicit. + +**Network and account limits:** run the loopback-only bridge in the same network namespace as ScopeWeaver; inside Docker, localhost means that container. Its downstream provider can still be a cloud service. SDK profiles are therefore rejected in **local-only LLM** mode. Bridge proxies are unsupported; configure downstream provider connectivity on the bridge. A free model ID means current availability, not subscription entitlement, permanent availability, or unlimited use. Confirm the provider's terms and current limits. The bridge does not select a paid fallback or import user accounts; any host-configured failover policy remains separate. Existing API and OpenCode Go profiles remain separate. + +**Downgrade preparation:** this change adds `opencode-sdk` to the profile-format database `CHECK` constraint. An older binary cannot start while SDK profiles are present. Back up the database and prepare conversion to an older supported profile format or removal of SDK profiles using the current version before downgrading. Verification used disposable databases; no live database migration was performed here. + ## OpenCode Go (opencode subscription gateway) Open **LLM → New → Configuration template** and choose **OpenCode Go · glm-5.3-flash**. diff --git a/docs/zh-Hans/llm-providers.md b/docs/zh-Hans/llm-providers.md index d71307d7..a063fa8a 100644 --- a/docs/zh-Hans/llm-providers.md +++ b/docs/zh-Hans/llm-providers.md @@ -6,7 +6,9 @@ 各语言版本请见本页上方导览。 -ScopeWeaver 支持四种配置格式:**Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses** 与原生 **Ollama**。每种格式保留自定义基础 URL、代理、会话头部、重试、速率与并行配置,因此云端供应商、网关与自行托管的 OpenAI 兼容服务器均维持既有运作。Ollama 为选用,云端供应商仍是默认兼容路径。 +ScopeWeaver 支持 **Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses**、原生 **Ollama** 和可选的 **OpenCode SDK 桥接器**。原有四种格式继续保留自定义基础 URL、代理、会话头部、重试、速率与并行设置;云端、网关与自行托管的 OpenAI 兼容服务器保持原有行为。SDK 使用独立凭证及下文的限制,不替代 API/OpenCode Go 路径。 + +SDK 模板也不发送请求;模型发现必须明确点击**读取模型**。 模板只填写**新配置表单**,不保存凭证、不启用配置、不修改既有配置。Ollama 模板亦从所选服务器读取已安装模型;云端模板不发出请求。应用后格式、基础 URL 与模型均可编辑。 @@ -40,6 +42,55 @@ GLM-5.3 接受文字,上下文为 1M token,且必须保持推理启用。支 已于 **2026-10-03** 对照官方文档验证。供应商可用性、帐号资格与政策可能改变。 + + +## OpenCode SDK 桥接器 + +`opencode-sdk` 是独立的可选配置格式,不是 OpenCode 的内置 HTTP API。ScopeWeaver 的 Norma 运行时继续管理对话、工具、审批、范围、重试与压缩;桥接器只完成一次模型步骤并收集宿主工具请求。 + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定的运行时 | 参数与历史边界 | +| --- | --- | --- | +| `native`(默认) | 官方 CLI、客户端、SDK **2.1.4** | 保留响应和宿主历史中的原始工具参数 JSON,包括 64 位整数;拒绝原始参数之外不安全的数值。 | +| `legacy` | SDK、CLI **1.19.4** | 只支持结构有效、安全可表示的 JSON;整数绝对值不超过 **9,007,199,254,740,991**。空白及等价 HTML/Unicode 转义可规范化,不保留原始字节;拒绝不安全整数和不透明/带签名思考历史。需要原始参数保真时用 `native`。 | + +原生模式通过实际固定 CLI 的 `serve --stdio`、`run --server --session` 及官方客户端管理会话、事件和中断。每次调用拥有私有临时 home/配置/数据路径和数据库。保留真实内置工具定义及原始实现;原生权限为 ASK,不使用 `--auto`,公开 `execute.before` 钩子在执行前阻止所有非宿主工具。只有收集的宿主请求交回 Norma 执行和审批。OpenCode 生成的模型/环境/工具上下文与宿主 system、历史共同保留。遇到无法受控的系统/MDM 配置来源时拒绝启动,不读取或修改它们;细节见适配器指南。 + +旧版自定义供应商使用显式 OpenAI 兼容配置。**两个运行时均不支持 Windows**,因为必需的 POSIX 进程树清理边界尚未实现。先前实际运行测试范围是 **macOS arm64**;Linux CI 已配置,但托管结果尚待验证。其他平台不声称已验证;其他 SDK 版本需单独验证。 + +按[适配器启动指南](../../adapters/opencode-sdk/README.zh-Hans.md)使用 **Bun 1.3.14**。桥接器使用自己的供应商配置,不导入用户 OpenCode 或云端已保存凭证。设置至少 **32 个非空白字符**的独立 `SCOPEWEAVER_OPENCODE_TOKEN`,在 **LLM → 新增 → 配置模板 → OpenCode SDK 桥接器**中填入;不要粘贴 OpenCode Go/Console API 密钥。 + +```sh +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +保留令牌供配置的**桥接令牌**字段使用。默认 `native`;启动前设 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` 可改用旧版。监听地址为 `127.0.0.1:4097`。通过 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` 指定专用 JSON 可配置显式供应商,必须使用适配器文档中的格式,不要指向用户认证文件。安装时保留可选平台依赖。 + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` 为 **0–86,400,000 毫秒**,默认 **0** 由宿主取消控制。请求体上限 **16 MiB**;超限明确失败。其余输出与清理限额见适配器指南。 + +| 字段 | 值 | +| --- | --- | +| 格式 | `opencode-sdk` | +| 桥接 URL | `http://127.0.0.1:4097` | +| 模型 | 点击**读取模型**选择或手动输入 `providerID/modelID` | +| 桥接令牌 | 桥接器的 `SCOPEWEAVER_OPENCODE_TOKEN`,不是云端 API 密钥 | +| 流式传输 | 开启,使用所选固定运行时 | +| 思考/推理强度 | 模板省略,仅设置模型支持的值 | +| 输出上限 | `0` 使用宿主调用路径的现有默认值;桥接器按原生模型声明的较小输出上限限制请求 | +| 代理/自定义会话头部 | 不支持;模板清除两者 | + +模板清除未保存的云端密钥,模型留空,**不发现、不推理、不保存、不启用**。填入令牌后明确点击**读取模型**,可不执行推理而查看目录;准备好后保存并单独启用。**测试连接**只在点击时发起模型请求,元数据发现不代表推理访问成功。 + +**先前免费模型验证 — 2026-10-25 KST:** 当目录列出时,可从**读取模型**中选择 `opencode/deepseek-v4.1-flash-free`。此前未登录的 macOS arm64 运行通过真正的原生 CLI 2.1.4 完成 UI 连接测试、手动 ASK 批准后的宿主 Bash 写入、精确的 `FREE_OK` 加换行文件内容及最终助手答案。共 **3 次推理 POST**,输出上限 **1,024**,关闭重试,没有付费回退,也未观察到内置工具执行。这只证明当时一个小型隔离流程;不代表本次重跑、永久可用或无限免费。当前模型、免费资格及限额仍需确认。旧版托管免费访问尚未验证。 + +编辑时,空令牌只在同一 SDK 地址保留已保存值。更改 URL 会清除输入的令牌;更改地址或格式后留空不会复用已保存凭证。`localhost`、`127.0.0.1`、`::1` 是不同凭证作用域。模型 ID 与输出限制以当前元数据为准。思考设置依赖模型/供应商支持,不支持时明确失败。 + +**网络与账户限制:** 桥接器必须和 ScopeWeaver 位于同一网络命名空间;容器中的 localhost 指容器本身。下游仍可能是云服务,所以**仅本地 LLM** 模式拒绝 SDK 配置。桥接器不支持代理;下游网络在桥接器侧配置。免费模型 ID 不证明订阅权限、持续提供或无限用量;确认服务条款和当前限额。桥接器不选择付费回退、不导入账户;宿主已配置的故障转移策略另行管理。现有 API/OpenCode Go 配置保持独立。 + +**降级准备:** 数据库配置格式的 `CHECK` 约束增加 `opencode-sdk`。仍有 SDK 配置时旧二进制不能启动。降级前备份数据库,在当前版本将 SDK 配置转换为旧支持格式或移除。先前验证使用临时数据库,没有迁移生产数据库。 + ## OpenCode Go(opencode 订阅网关) diff --git a/docs/zh-Hant/llm-providers.md b/docs/zh-Hant/llm-providers.md index ce2a817b..444e5ada 100644 --- a/docs/zh-Hant/llm-providers.md +++ b/docs/zh-Hant/llm-providers.md @@ -6,7 +6,9 @@ 各語言版本請見本頁上方導覽。 -ScopeWeaver 支援四種設定檔格式:**Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses** 與原生 **Ollama**。每種格式保留自訂基底 URL、代理、工作階段標頭、重試、速率與並行設定,因此雲端供應商、閘道與自行代管的 OpenAI 相容伺服器均維持既有運作。Ollama 為選用,雲端供應商仍是預設相容路徑。 +ScopeWeaver 支援 **Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses**、原生 **Ollama** 和選用的 **OpenCode SDK 橋接器**。原有四種格式繼續保留自訂基底 URL、代理、工作階段標頭、重試、速率與並行設定;雲端、閘道與自行代管的 OpenAI 相容伺服器維持原有行為。SDK 使用獨立憑證及下文的限制,不取代 API/OpenCode Go 路徑。 + +SDK 範本也不發送請求;模型探索必須明確點選**讀取模型**。 範本只填寫**新設定檔表單**,不儲存憑證、不啟用設定檔、不修改既有設定檔。Ollama 範本亦從所選伺服器讀取已安裝模型;雲端範本不發出請求。套用後格式、基底 URL 與模型均可編輯。 @@ -40,6 +42,55 @@ GLM-5.3 接受文字,上下文為 1M token,且必須保持推理啟用。支 已於 **2026-10-03** 對照官方文件驗證。供應商可用性、帳號資格與政策可能改變。 + + +## OpenCode SDK 橋接器 + +`opencode-sdk` 是獨立的可選設定格式,不是 OpenCode 的內建 HTTP API。ScopeWeaver 的 Norma 運行時繼續管理對話、工具、審批、範圍、重試與壓縮;橋接器只完成一次模型步驟並收集宿主工具請求。 + +| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定的運行時 | 參數與歷史邊界 | +| --- | --- | --- | +| `native`(預設) | 官方 CLI、客戶端、SDK **2.1.4** | 保留響應和宿主歷史中的原始工具參數 JSON,包括 64 位整數;拒絕原始參數之外不安全的數值。 | +| `legacy` | SDK、CLI **1.19.4** | 只支持結構有效、安全可表示的 JSON;整數絕對值不超過 **9,007,199,254,740,991**。空白及等價 HTML/Unicode 轉義可規範化,不保留原始位元組;拒絕不安全整數和不透明/帶簽名思考歷史。需要原始參數保真時用 `native`。 | + +原生模式通過實際固定 CLI 的 `serve --stdio`、`run --server --session` 及官方客戶端管理工作階段、事件和中斷。每次調用擁有私有臨時 home/設定/資料路徑和資料庫。保留真實內建工具定義及原始實現;原生權限為 ASK,不使用 `--auto`,公開 `execute.before` 鈎子在執行前阻止所有非宿主工具。只有收集的宿主請求交回 Norma 執行和審批。OpenCode 生成的模型/環境/工具上下文與宿主 system、歷史共同保留。遇到無法受控的系統/MDM 設定來源時拒絕啓動,不讀取或修改它們;細節見適配器指南。 + +舊版自定義供應商使用顯式 OpenAI 兼容設定。**兩個運行時均不支持 Windows**,因為必需的 POSIX 進程樹清理邊界尚未實現。先前實際運行測試範圍是 **macOS arm64**;Linux CI 已設定,但托管結果尚待驗證。其他平台不聲稱已驗證;其他 SDK 版本需單獨驗證。 + +按[適配器啓動指南](../../adapters/opencode-sdk/README.zh-Hant.md)使用 **Bun 1.3.14**。橋接器使用自己的供應商設定,不導入用戶 OpenCode 或雲端已保存憑證。設置至少 **32 個非空白字元**的獨立 `SCOPEWEAVER_OPENCODE_TOKEN`,在 **LLM → 新增 → 設定模板 → OpenCode SDK 橋接器**中填入;不要粘貼 OpenCode Go/Console API 密鑰。 + +```sh +cd adapters/opencode-sdk +bun install --frozen-lockfile --ignore-scripts +export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)" +bun run start +``` + +保留權杖供設定的**橋接權杖**字段使用。預設 `native`;啓動前設 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` 可改用舊版。監聽地址為 `127.0.0.1:4097`。通過 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` 指定專用 JSON 可設定顯式供應商,必須使用適配器文檔中的格式,不要指向用戶認證文件。安裝時保留可選平台依賴。 + +`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` 為 **0–86,400,000 毫秒**,預設 **0** 由宿主取消控制。請求體上限 **16 MiB**;超限明確失敗。其餘輸出與清理限額見適配器指南。 + +| 字段 | 值 | +| --- | --- | +| 格式 | `opencode-sdk` | +| 橋接 URL | `http://127.0.0.1:4097` | +| 模型 | 點擊**讀取模型**選擇或手動輸入 `providerID/modelID` | +| 橋接權杖 | 橋接器的 `SCOPEWEAVER_OPENCODE_TOKEN`,不是雲端 API 密鑰 | +| 流式傳輸 | 開啓,使用所選固定運行時 | +| 思考/推理強度 | 模板省略,僅設置模型支持的值 | +| 輸出上限 | `0` 使用宿主調用路徑的現有預設值;橋接器按原生模型聲明的較小輸出上限限制請求 | +| 代理/自定義工作階段標頭 | 不支持;模板清除兩者 | + +模板清除未保存的雲端密鑰,模型留空,**不發現、不推理、不保存、不啓用**。填入權杖後明確點擊**讀取模型**,可不執行推理而查看目錄;準備好後保存並單獨啓用。**測試連接**只在點擊時發起模型請求,元資料發現不代表推理訪問成功。 + +**先前免費模型驗證 — 2026-10-25 KST:** 當目錄列出時,可從**讀取模型**中選擇 `opencode/deepseek-v4.1-flash-free`。此前未登錄的 macOS arm64 運行通過真正的原生 CLI 2.1.4 完成 UI 連接測試、手動 ASK 批准後的宿主 Bash 寫入、精確的 `FREE_OK` 加換行文件內容及最終助手答案。共 **3 次推理 POST**,輸出上限 **1,024**,關閉重試,沒有付費回退,也未觀察到內建工具執行。這只證明當時一個小型隔離流程;不代表本次重跑、永久可用或無限免費。當前模型、免費資格及限額仍需確認。舊版托管免費訪問尚未驗證。 + +編輯時,空權杖只在同一 SDK 地址保留已保存值。更改 URL 會清除輸入的權杖;更改地址或格式後留空不會復用已保存憑證。`localhost`、`127.0.0.1`、`::1` 是不同憑證作用域。模型 ID 與輸出限制以當前元資料為準。思考設置依賴模型/供應商支持,不支持時明確失敗。 + +**網絡與賬戶限制:** 橋接器必須和 ScopeWeaver 位於同一網絡命名空間;容器中的 localhost 指容器本身。下游仍可能是雲服務,所以**僅本地 LLM** 模式拒絕 SDK 設定。橋接器不支持代理;下游網絡在橋接器側設定。免費模型 ID 不證明訂閱權限、持續提供或無限用量;確認服務條款和當前限額。橋接器不選擇付費回退、不導入賬戶;宿主已設定的故障轉移策略另行管理。現有 API/OpenCode Go 設定保持獨立。 + +**降級準備:** 資料庫設定格式的 `CHECK` 約束增加 `opencode-sdk`。仍有 SDK 設定時舊二進制不能啓動。降級前備份資料庫,在當前版本將 SDK 設定轉換為舊支持格式或移除。先前驗證使用臨時資料庫,沒有遷移生產資料庫。 + ## OpenCode Go(opencode 訂閱閘道) diff --git a/locale/es.json b/locale/es.json index 8dc051a9..260e3b4a 100644 --- a/locale/es.json +++ b/locale/es.json @@ -1961,5 +1961,61 @@ "🔴 Critical": "🔴 Crítica", "🔵 Low": "🔵 Baja", "🟠 High": "🟠 Alta", - "🟡 Medium": "🟡 Media" + "🟡 Medium": "🟡 Media", + "An unexpected additional SDK model request was blocked.": "Se ha bloqueado una solicitud adicional inesperada al modelo del SDK.", + "Bridge authentication required.": "Se requiere autenticación del puente.", + "Invalid bridge provider configuration.": "La configuración del proveedor del puente no es válida.", + "Invalid completion request.": "La solicitud de generación no es válida.", + "Only non-browser loopback requests are accepted.": "Solo se aceptan solicitudes de bucle local de clientes que no sean navegadores.", + "OpenCode SDK %s: %w": "OpenCode SDK %s: %w", + "OpenCode SDK HTTP %d: %w": "OpenCode SDK HTTP %d: %w", + "OpenCode SDK bridge proxy settings are not allowed": "No se permite configurar un proxy en el puente de OpenCode SDK", + "OpenCode SDK event after message_stop": "Hay un evento de OpenCode SDK después de message_stop", + "OpenCode SDK event after terminal delta": "Hay un evento de OpenCode SDK después del delta final", + "OpenCode SDK max_tokens must be nonnegative": "max_tokens de OpenCode SDK debe ser un número no negativo", + "OpenCode SDK model must be providerID/modelID": "El modelo de OpenCode SDK debe tener el formato providerID/modelID", + "OpenCode SDK request exceeds 32 MiB": "La solicitud de OpenCode SDK supera los 32 MiB", + "OpenCode SDK request failed: %w": "La solicitud de OpenCode SDK ha fallado: %w", + "OpenCode SDK response exceeds 32 MiB": "La respuesta de OpenCode SDK supera los 32 MiB", + "OpenCode SDK response has no usage": "La respuesta de OpenCode SDK no incluye datos de uso", + "OpenCode SDK stream ended without message_stop": "El flujo de OpenCode SDK ha terminado sin message_stop", + "OpenCode SDK stream exceeds 32 MiB": "El flujo de OpenCode SDK supera los 32 MiB", + "OpenCode SDK stream failed: %w": "El flujo de OpenCode SDK ha fallado: %w", + "OpenCode SDK stream missing message_start": "El flujo de OpenCode SDK no incluye message_start", + "OpenCode SDK stream missing terminal usage": "El flujo de OpenCode SDK no incluye los datos de uso finales", + "OpenCode SDK stream must use application/x-ndjson": "El flujo de OpenCode SDK debe usar application/x-ndjson", + "OpenCode SDK tool input without tool start": "Hay una entrada de herramienta de OpenCode SDK sin un evento de inicio de herramienta", + "OpenCode SDK tool stop reason does not match tool calls": "El motivo de finalización de la herramienta de OpenCode SDK no coincide con las llamadas a herramientas", + "Request body exceeds the bridge limit.": "El cuerpo de la solicitud supera el límite del puente.", + "Route not found.": "No se ha encontrado la ruta.", + "The SDK request failed. Check the selected model and provider configuration.": "La solicitud del SDK ha fallado. Compruebe el modelo seleccionado y la configuración del proveedor.", + "The SDK request timed out.": "Se ha agotado el tiempo de espera de la solicitud del SDK.", + "The SDK response could not be represented safely.": "No se ha podido representar la respuesta del SDK de forma segura.", + "The hosted free tier is restricted for this SDK runtime.": "El uso del nivel gratuito alojado está restringido para este entorno de ejecución del SDK.", + "The request was canceled.": "La solicitud se ha cancelado.", + "The selected model is unavailable in this bridge.": "El modelo seleccionado no está disponible en el puente.", + "The upstream model reported that the context length was exceeded.": "El modelo del proveedor ha informado de que se ha superado el límite de longitud del contexto.", + "The upstream provider reported exhausted quota or billing credit.": "El proveedor ha informado de que se ha agotado la cuota o el saldo de facturación.", + "This input cannot be represented safely by the SDK.": "El SDK no puede representar esta entrada de forma segura.", + "This request ID is already active.": "Este ID de solicitud ya está activo.", + "Unsupported SDK/runtime pair or platform. Use a tested pinned runtime.": "Esta combinación de SDK y entorno de ejecución, o esta plataforma, no es compatible. Utilice la versión fija y probada del entorno de ejecución.", + "a separate OpenCode bridge token is required": "Se requiere un token independiente para el puente de OpenCode", + "bridge URL must be a loopback HTTP(S) URL without credentials, query or fragment": "La URL del puente debe ser una URL HTTP(S) de bucle local sin credenciales, consulta ni fragmento", + "bridge URL must use localhost or a literal loopback address": "La URL del puente debe usar localhost o una dirección IP literal de bucle local", + "duplicate OpenCode SDK message_start": "message_start de OpenCode SDK está duplicado", + "invalid OpenCode SDK JSON: %w": "El JSON de OpenCode SDK no es válido: %w", + "invalid OpenCode SDK message, stop reason or usage": "El mensaje, el motivo de finalización o el uso de OpenCode SDK no son válidos", + "invalid OpenCode SDK provider/model ID": "El ID de proveedor/modelo de OpenCode SDK no es válido", + "invalid OpenCode SDK stream JSON: %w": "El JSON del flujo de OpenCode SDK no es válido: %w", + "invalid OpenCode SDK terminal delta": "El delta final de OpenCode SDK no es válido", + "invalid OpenCode SDK tool arguments": "Los argumentos de la herramienta de OpenCode SDK no son válidos", + "invalid OpenCode SDK tool input JSON": "El JSON de entrada de la herramienta de OpenCode SDK no es válido", + "invalid OpenCode SDK trailing JSON": "El final de la respuesta de OpenCode SDK contiene JSON no válido", + "invalid OpenCode SDK usage": "Los datos de uso de OpenCode SDK no son válidos", + "invalid or unknown OpenCode SDK host tool call": "La llamada a una herramienta del host de OpenCode SDK no es válida o es desconocida", + "local_llm_only: OpenCode SDK downstream inference cannot be restricted by the loopback bridge": "local_llm_only: el puente de bucle local no puede restringir la inferencia posterior de OpenCode SDK", + "missing OpenCode SDK tool input": "Falta la entrada de la herramienta de OpenCode SDK", + "unknown OpenCode SDK event type": "Tipo de evento de OpenCode SDK desconocido", + "unsupported OpenCode SDK bridge protocol, runtime version or capture-only capabilities": "El protocolo del puente de OpenCode SDK, la versión del entorno de ejecución o las funciones de solo captura no son compatibles", + "unsupported OpenCode SDK content block": "El bloque de contenido de OpenCode SDK no es compatible" } diff --git a/locale/ja.json b/locale/ja.json index 8539f5bd..2db9618c 100644 --- a/locale/ja.json +++ b/locale/ja.json @@ -1961,5 +1961,61 @@ "🔴 Critical": "🔴 重大", "🔵 Low": "🔵 低", "🟠 High": "🟠 高", - "🟡 Medium": "🟡 中" + "🟡 Medium": "🟡 中", + "An unexpected additional SDK model request was blocked.": "予期しない追加の SDK モデルリクエストをブロックしました。", + "Bridge authentication required.": "ブリッジの認証が必要です。", + "Invalid bridge provider configuration.": "ブリッジのプロバイダー設定が無効です。", + "Invalid completion request.": "補完リクエストが無効です。", + "Only non-browser loopback requests are accepted.": "ブラウザー以外のクライアントからのループバックリクエストのみ受け付けます。", + "OpenCode SDK %s: %w": "OpenCode SDK %s: %w", + "OpenCode SDK HTTP %d: %w": "OpenCode SDK HTTP %d: %w", + "OpenCode SDK bridge proxy settings are not allowed": "OpenCode SDK ブリッジではプロキシを設定できません", + "OpenCode SDK event after message_stop": "OpenCode SDK の message_stop の後にイベントがあります", + "OpenCode SDK event after terminal delta": "OpenCode SDK の終了デルタの後にイベントがあります", + "OpenCode SDK max_tokens must be nonnegative": "OpenCode SDK の max_tokens は 0 以上である必要があります", + "OpenCode SDK model must be providerID/modelID": "OpenCode SDK のモデルは providerID/modelID 形式で指定する必要があります", + "OpenCode SDK request exceeds 32 MiB": "OpenCode SDK リクエストが 32 MiB を超えています", + "OpenCode SDK request failed: %w": "OpenCode SDK リクエストに失敗しました: %w", + "OpenCode SDK response exceeds 32 MiB": "OpenCode SDK の応答が 32 MiB を超えています", + "OpenCode SDK response has no usage": "OpenCode SDK の応答に使用量がありません", + "OpenCode SDK stream ended without message_stop": "OpenCode SDK のストリームが message_stop なしで終了しました", + "OpenCode SDK stream exceeds 32 MiB": "OpenCode SDK のストリームが 32 MiB を超えています", + "OpenCode SDK stream failed: %w": "OpenCode SDK のストリームに失敗しました: %w", + "OpenCode SDK stream missing message_start": "OpenCode SDK のストリームに message_start がありません", + "OpenCode SDK stream missing terminal usage": "OpenCode SDK のストリームに最終使用量がありません", + "OpenCode SDK stream must use application/x-ndjson": "OpenCode SDK のストリームには application/x-ndjson を使用する必要があります", + "OpenCode SDK tool input without tool start": "OpenCode SDK のツール入力に対応するツール開始イベントがありません", + "OpenCode SDK tool stop reason does not match tool calls": "OpenCode SDK のツールの停止理由がツール呼び出しと一致しません", + "Request body exceeds the bridge limit.": "リクエスト本文がブリッジの上限を超えています。", + "Route not found.": "ルートが見つかりません。", + "The SDK request failed. Check the selected model and provider configuration.": "SDK リクエストに失敗しました。選択したモデルとプロバイダーの設定を確認してください。", + "The SDK request timed out.": "SDK リクエストがタイムアウトしました。", + "The SDK response could not be represented safely.": "SDK の応答を安全に表現できませんでした。", + "The hosted free tier is restricted for this SDK runtime.": "この SDK ランタイムでは、ホスト型サービスの無料プランの利用が制限されています。", + "The request was canceled.": "リクエストがキャンセルされました。", + "The selected model is unavailable in this bridge.": "選択したモデルはブリッジで利用できません。", + "The upstream model reported that the context length was exceeded.": "上流のモデルがコンテキスト長の上限超過を報告しました。", + "The upstream provider reported exhausted quota or billing credit.": "上流のプロバイダーがクォータまたは請求クレジットを使い切ったことを報告しました。", + "This input cannot be represented safely by the SDK.": "SDK でこの入力を安全に表現できません。", + "This request ID is already active.": "このリクエスト ID はすでに使用中です。", + "Unsupported SDK/runtime pair or platform. Use a tested pinned runtime.": "この SDK とランタイムの組み合わせ、またはプラットフォームはサポートされていません。テスト済みの固定バージョンのランタイムを使用してください。", + "a separate OpenCode bridge token is required": "専用の OpenCode ブリッジトークンが必要です", + "bridge URL must be a loopback HTTP(S) URL without credentials, query or fragment": "ブリッジ URL は、認証情報、クエリ、フラグメントを含まないループバック HTTP(S) URL である必要があります", + "bridge URL must use localhost or a literal loopback address": "ブリッジ URL には localhost またはループバック IP アドレスのリテラルを使用する必要があります", + "duplicate OpenCode SDK message_start": "OpenCode SDK の message_start が重複しています", + "invalid OpenCode SDK JSON: %w": "OpenCode SDK の JSON が無効です: %w", + "invalid OpenCode SDK message, stop reason or usage": "OpenCode SDK のメッセージ、停止理由、または使用量が無効です", + "invalid OpenCode SDK provider/model ID": "OpenCode SDK のプロバイダー/モデル ID が無効です", + "invalid OpenCode SDK stream JSON: %w": "OpenCode SDK のストリーム JSON が無効です: %w", + "invalid OpenCode SDK terminal delta": "OpenCode SDK の終了デルタが無効です", + "invalid OpenCode SDK tool arguments": "OpenCode SDK のツール引数が無効です", + "invalid OpenCode SDK tool input JSON": "OpenCode SDK のツール入力 JSON が無効です", + "invalid OpenCode SDK trailing JSON": "OpenCode SDK の応答末尾に無効な JSON があります", + "invalid OpenCode SDK usage": "OpenCode SDK の使用量が無効です", + "invalid or unknown OpenCode SDK host tool call": "OpenCode SDK のホストツール呼び出しが無効か、不明です", + "local_llm_only: OpenCode SDK downstream inference cannot be restricted by the loopback bridge": "local_llm_only: ループバックブリッジでは OpenCode SDK の下流の推論を制限できません", + "missing OpenCode SDK tool input": "OpenCode SDK のツール入力がありません", + "unknown OpenCode SDK event type": "不明な OpenCode SDK イベントタイプです", + "unsupported OpenCode SDK bridge protocol, runtime version or capture-only capabilities": "この OpenCode SDK ブリッジプロトコル、ランタイムのバージョン、またはキャプチャ専用機能はサポートされていません", + "unsupported OpenCode SDK content block": "サポートされていない OpenCode SDK のコンテンツブロックです" } diff --git a/locale/opencode_sdk.go b/locale/opencode_sdk.go new file mode 100644 index 00000000..f1fd5f83 --- /dev/null +++ b/locale/opencode_sdk.go @@ -0,0 +1,62 @@ +package locale + +func init() { + RegisterAll(map[string]map[Lang]string{ + "The hosted free tier is restricted for this SDK runtime.": {En: "The hosted free tier is restricted for this SDK runtime.", Ko: "이 SDK 런타임에서는 호스팅 무료 요금제 이용이 제한됩니다.", ZhHans: "此 SDK 运行时使用托管免费套餐受到限制。"}, + "The upstream model reported that the context length was exceeded.": {En: "The upstream model reported that the context length was exceeded.", Ko: "상위 모델이 컨텍스트 길이 초과를 보고했습니다.", ZhHans: "上游模型报告上下文长度已超出限制。"}, + "Bridge authentication required.": {En: "Bridge authentication required.", Ko: "브리지 인증이 필요합니다.", ZhHans: "需要桥接身份验证。"}, + "Only non-browser loopback requests are accepted.": {En: "Only non-browser loopback requests are accepted.", Ko: "브라우저가 아닌 클라이언트의 루프백 요청만 허용됩니다.", ZhHans: "仅接受非浏览器客户端的回环请求。"}, + "Invalid completion request.": {En: "Invalid completion request.", Ko: "완성 요청이 올바르지 않습니다.", ZhHans: "补全请求无效。"}, + "This input cannot be represented safely by the SDK.": {En: "This input cannot be represented safely by the SDK.", Ko: "SDK가 이 입력을 안전하게 표현할 수 없습니다.", ZhHans: "SDK 无法安全表示此输入。"}, + "Unsupported SDK/runtime pair or platform. Use a tested pinned runtime.": {En: "Unsupported SDK/runtime pair or platform. Use a tested pinned runtime.", Ko: "지원하지 않는 SDK·런타임 조합 또는 플랫폼입니다. 검증된 고정 버전 런타임을 사용하세요.", ZhHans: "不支持此 SDK 与运行时组合或平台。请使用经过测试的固定版本运行时。"}, + "The selected model is unavailable in this bridge.": {En: "The selected model is unavailable in this bridge.", Ko: "이 브리지에서 선택한 모델을 사용할 수 없습니다.", ZhHans: "此桥接中无法使用所选模型。"}, + "This request ID is already active.": {En: "This request ID is already active.", Ko: "이 요청 ID가 이미 사용 중입니다.", ZhHans: "此请求 ID 已在使用中。"}, + "The request was canceled.": {En: "The request was canceled.", Ko: "요청이 취소되었습니다.", ZhHans: "请求已取消。"}, + "The SDK request timed out.": {En: "The SDK request timed out.", Ko: "SDK 요청 시간이 초과되었습니다.", ZhHans: "SDK 请求超时。"}, + "The SDK request failed. Check the selected model and provider configuration.": {En: "The SDK request failed. Check the selected model and provider configuration.", Ko: "SDK 요청에 실패했습니다. 선택한 모델과 제공자 설정을 확인하세요.", ZhHans: "SDK 请求失败。请检查所选模型和提供者配置。"}, + "The SDK response could not be represented safely.": {En: "The SDK response could not be represented safely.", Ko: "SDK 응답을 안전하게 표현할 수 없습니다.", ZhHans: "无法安全表示 SDK 响应。"}, + "Invalid bridge provider configuration.": {En: "Invalid bridge provider configuration.", Ko: "브리지 제공자 설정이 올바르지 않습니다.", ZhHans: "桥接提供者配置无效。"}, + "An unexpected additional SDK model request was blocked.": {En: "An unexpected additional SDK model request was blocked.", Ko: "예상하지 못한 추가 SDK 모델 요청이 차단되었습니다.", ZhHans: "意外的额外 SDK 模型请求已被阻止。"}, + "The upstream provider reported exhausted quota or billing credit.": {En: "The upstream provider reported exhausted quota or billing credit.", Ko: "상위 제공자가 할당량 또는 결제 크레딧 소진을 보고했습니다.", ZhHans: "上游提供者报告配额或计费余额已耗尽。"}, + "Route not found.": {En: "Route not found.", Ko: "경로를 찾을 수 없습니다.", ZhHans: "找不到路由。"}, + "Request body exceeds the bridge limit.": {En: "Request body exceeds the bridge limit.", Ko: "요청 본문이 브리지 제한을 초과합니다.", ZhHans: "请求正文超过桥接限制。"}, + "bridge URL must be a loopback HTTP(S) URL without credentials, query or fragment": {En: "bridge URL must be a loopback HTTP(S) URL without credentials, query or fragment", Ko: "브리지 URL은 인증 정보·쿼리·프래그먼트가 없는 루프백 HTTP(S) URL이어야 합니다", ZhHans: "桥接 URL 必须是无凭据、查询或片段的回环 HTTP(S) URL"}, + "bridge URL must use localhost or a literal loopback address": {En: "bridge URL must use localhost or a literal loopback address", Ko: "브리지 URL에는 localhost 또는 루프백 IP 주소를 사용해야 합니다", ZhHans: "桥接 URL 必须使用 localhost 或回环 IP 地址"}, + "a separate OpenCode bridge token is required": {En: "a separate OpenCode bridge token is required", Ko: "별도의 OpenCode 브리지 토큰이 필요합니다", ZhHans: "需要单独的 OpenCode 桥接令牌"}, + "local_llm_only: OpenCode SDK downstream inference cannot be restricted by the loopback bridge": {En: "local_llm_only: OpenCode SDK downstream inference cannot be restricted by the loopback bridge", Ko: "local_llm_only: 루프백 브리지는 OpenCode SDK의 후속 추론 연결을 제한할 수 없습니다", ZhHans: "local_llm_only:回环桥接无法限制 OpenCode SDK 的下游推理连接"}, + "OpenCode SDK request exceeds 32 MiB": {En: "OpenCode SDK request exceeds 32 MiB", Ko: "OpenCode SDK 요청이 32 MiB를 초과합니다", ZhHans: "OpenCode SDK 请求超过 32 MiB"}, + "OpenCode SDK request failed: %w": {En: "OpenCode SDK request failed: %w", Ko: "OpenCode SDK 요청 실패: %w", ZhHans: "OpenCode SDK 请求失败:%w"}, + "OpenCode SDK HTTP %d: %w": {En: "OpenCode SDK HTTP %d: %w", Ko: "OpenCode SDK HTTP %d: %w", ZhHans: "OpenCode SDK HTTP %d:%w"}, + "OpenCode SDK response exceeds 32 MiB": {En: "OpenCode SDK response exceeds 32 MiB", Ko: "OpenCode SDK 응답이 32 MiB를 초과합니다", ZhHans: "OpenCode SDK 响应超过 32 MiB"}, + "invalid OpenCode SDK JSON: %w": {En: "invalid OpenCode SDK JSON: %w", Ko: "OpenCode SDK JSON이 올바르지 않습니다: %w", ZhHans: "OpenCode SDK JSON 无效:%w"}, + "invalid OpenCode SDK trailing JSON": {En: "invalid OpenCode SDK trailing JSON", Ko: "OpenCode SDK 응답 끝에 올바르지 않은 JSON이 있습니다", ZhHans: "OpenCode SDK 响应尾部包含无效 JSON"}, + "unsupported OpenCode SDK bridge protocol, runtime version or capture-only capabilities": {En: "unsupported OpenCode SDK bridge protocol, runtime version or capture-only capabilities", Ko: "OpenCode SDK 브리지 프로토콜·런타임 버전 또는 캡처 전용 기능을 지원하지 않습니다", ZhHans: "不支持此 OpenCode SDK 桥接协议、运行时版本或仅捕获功能"}, + "invalid OpenCode SDK provider/model ID": {En: "invalid OpenCode SDK provider/model ID", Ko: "OpenCode SDK 제공자/모델 ID가 올바르지 않습니다", ZhHans: "OpenCode SDK 提供者/模型 ID 无效"}, + "OpenCode SDK model must be providerID/modelID": {En: "OpenCode SDK model must be providerID/modelID", Ko: "OpenCode SDK 모델은 providerID/modelID 형식이어야 합니다", ZhHans: "OpenCode SDK 模型必须采用 providerID/modelID 格式"}, + "OpenCode SDK max_tokens must be nonnegative": {En: "OpenCode SDK max_tokens must be nonnegative", Ko: "OpenCode SDK max_tokens는 0 이상이어야 합니다", ZhHans: "OpenCode SDK max_tokens 必须为非负数"}, + "invalid OpenCode SDK message, stop reason or usage": {En: "invalid OpenCode SDK message, stop reason or usage", Ko: "OpenCode SDK 메시지·종료 사유 또는 사용량이 올바르지 않습니다", ZhHans: "OpenCode SDK 消息、停止原因或用量无效"}, + "invalid or unknown OpenCode SDK host tool call": {En: "invalid or unknown OpenCode SDK host tool call", Ko: "OpenCode SDK 호스트 도구 호출이 올바르지 않거나 알 수 없습니다", ZhHans: "OpenCode SDK 主机工具调用无效或未知"}, + "invalid OpenCode SDK tool arguments": {En: "invalid OpenCode SDK tool arguments", Ko: "OpenCode SDK 도구 인수가 올바르지 않습니다", ZhHans: "OpenCode SDK 工具参数无效"}, + "unsupported OpenCode SDK content block": {En: "unsupported OpenCode SDK content block", Ko: "지원하지 않는 OpenCode SDK 콘텐츠 블록입니다", ZhHans: "不支持此 OpenCode SDK 内容块"}, + "OpenCode SDK tool stop reason does not match tool calls": {En: "OpenCode SDK tool stop reason does not match tool calls", Ko: "OpenCode SDK의 도구 종료 사유와 도구 호출이 일치하지 않습니다", ZhHans: "OpenCode SDK 工具停止原因与工具调用不匹配"}, + "OpenCode SDK response has no usage": {En: "OpenCode SDK response has no usage", Ko: "OpenCode SDK 응답에 사용량이 없습니다", ZhHans: "OpenCode SDK 响应缺少用量"}, + "OpenCode SDK stream must use application/x-ndjson": {En: "OpenCode SDK stream must use application/x-ndjson", Ko: "OpenCode SDK 스트림은 application/x-ndjson을 사용해야 합니다", ZhHans: "OpenCode SDK 流必须使用 application/x-ndjson"}, + "OpenCode SDK stream exceeds 32 MiB": {En: "OpenCode SDK stream exceeds 32 MiB", Ko: "OpenCode SDK 스트림이 32 MiB를 초과합니다", ZhHans: "OpenCode SDK 流超过 32 MiB"}, + "invalid OpenCode SDK stream JSON: %w": {En: "invalid OpenCode SDK stream JSON: %w", Ko: "OpenCode SDK 스트림 JSON이 올바르지 않습니다: %w", ZhHans: "OpenCode SDK 流 JSON 无效:%w"}, + "OpenCode SDK event after message_stop": {En: "OpenCode SDK event after message_stop", Ko: "OpenCode SDK message_stop 뒤에 이벤트가 있습니다", ZhHans: "OpenCode SDK message_stop 后出现事件"}, + "OpenCode SDK %s: %w": {En: "OpenCode SDK %s: %w", Ko: "OpenCode SDK %s: %w", ZhHans: "OpenCode SDK %s:%w"}, + "OpenCode SDK event after terminal delta": {En: "OpenCode SDK event after terminal delta", Ko: "OpenCode SDK 종료 델타 뒤에 이벤트가 있습니다", ZhHans: "OpenCode SDK 终止增量后出现事件"}, + "OpenCode SDK stream missing message_start": {En: "OpenCode SDK stream missing message_start", Ko: "OpenCode SDK 스트림에 message_start가 없습니다", ZhHans: "OpenCode SDK 流缺少 message_start"}, + "invalid OpenCode SDK usage": {En: "invalid OpenCode SDK usage", Ko: "OpenCode SDK 사용량이 올바르지 않습니다", ZhHans: "OpenCode SDK 用量无效"}, + "duplicate OpenCode SDK message_start": {En: "duplicate OpenCode SDK message_start", Ko: "OpenCode SDK message_start가 중복되었습니다", ZhHans: "OpenCode SDK message_start 重复"}, + "OpenCode SDK tool input without tool start": {En: "OpenCode SDK tool input without tool start", Ko: "도구 시작 없이 OpenCode SDK 도구 입력이 왔습니다", ZhHans: "OpenCode SDK 工具输入缺少工具开始事件"}, + "invalid OpenCode SDK terminal delta": {En: "invalid OpenCode SDK terminal delta", Ko: "OpenCode SDK 종료 델타가 올바르지 않습니다", ZhHans: "OpenCode SDK 终止增量无效"}, + "invalid OpenCode SDK tool input JSON": {En: "invalid OpenCode SDK tool input JSON", Ko: "OpenCode SDK 도구 입력 JSON이 올바르지 않습니다", ZhHans: "OpenCode SDK 工具输入 JSON 无效"}, + "missing OpenCode SDK tool input": {En: "missing OpenCode SDK tool input", Ko: "OpenCode SDK 도구 입력이 없습니다", ZhHans: "缺少 OpenCode SDK 工具输入"}, + "OpenCode SDK stream missing terminal usage": {En: "OpenCode SDK stream missing terminal usage", Ko: "OpenCode SDK 스트림에 최종 사용량이 없습니다", ZhHans: "OpenCode SDK 流缺少终止用量"}, + "unknown OpenCode SDK event type": {En: "unknown OpenCode SDK event type", Ko: "알 수 없는 OpenCode SDK 이벤트 유형", ZhHans: "未知的 OpenCode SDK 事件类型"}, + "OpenCode SDK stream failed: %w": {En: "OpenCode SDK stream failed: %w", Ko: "OpenCode SDK 스트림 실패: %w", ZhHans: "OpenCode SDK 流失败:%w"}, + "OpenCode SDK stream ended without message_stop": {En: "OpenCode SDK stream ended without message_stop", Ko: "OpenCode SDK 스트림이 message_stop 없이 끝났습니다", ZhHans: "OpenCode SDK 流结束时缺少 message_stop"}, + "OpenCode SDK bridge proxy settings are not allowed": {En: "OpenCode SDK bridge proxy settings are not allowed", Ko: "OpenCode SDK 브리지에는 프록시를 설정할 수 없습니다", ZhHans: "OpenCode SDK 桥接不允许设置代理"}, + }) +} diff --git a/locale/zh-Hans.json b/locale/zh-Hans.json index 672ee04b..0f65c6a3 100644 --- a/locale/zh-Hans.json +++ b/locale/zh-Hans.json @@ -1909,5 +1909,61 @@ "context summary failed or did not reduce context": "上下文摘要失败或未缩小上下文", "summary and protected exchanges exceed the model input budget": "摘要与受保护的对话超出模型输入预算", "invalid prepared transcript owner": "待发布对话记录的所属会话无效", - "task session required": "需要任务对话会话" + "task session required": "需要任务对话会话", + "An unexpected additional SDK model request was blocked.": "意外的额外 SDK 模型请求已被阻止。", + "Bridge authentication required.": "需要桥接身份验证。", + "Invalid bridge provider configuration.": "桥接提供者配置无效。", + "Invalid completion request.": "补全请求无效。", + "Only non-browser loopback requests are accepted.": "仅接受非浏览器客户端的回环请求。", + "OpenCode SDK %s: %w": "OpenCode SDK %s:%w", + "OpenCode SDK HTTP %d: %w": "OpenCode SDK HTTP %d:%w", + "OpenCode SDK bridge proxy settings are not allowed": "OpenCode SDK 桥接不允许设置代理", + "OpenCode SDK event after message_stop": "OpenCode SDK message_stop 后出现事件", + "OpenCode SDK event after terminal delta": "OpenCode SDK 终止增量后出现事件", + "OpenCode SDK max_tokens must be nonnegative": "OpenCode SDK max_tokens 必须为非负数", + "OpenCode SDK model must be providerID/modelID": "OpenCode SDK 模型必须采用 providerID/modelID 格式", + "OpenCode SDK request exceeds 32 MiB": "OpenCode SDK 请求超过 32 MiB", + "OpenCode SDK request failed: %w": "OpenCode SDK 请求失败:%w", + "OpenCode SDK response exceeds 32 MiB": "OpenCode SDK 响应超过 32 MiB", + "OpenCode SDK response has no usage": "OpenCode SDK 响应缺少用量", + "OpenCode SDK stream ended without message_stop": "OpenCode SDK 流结束时缺少 message_stop", + "OpenCode SDK stream exceeds 32 MiB": "OpenCode SDK 流超过 32 MiB", + "OpenCode SDK stream failed: %w": "OpenCode SDK 流失败:%w", + "OpenCode SDK stream missing message_start": "OpenCode SDK 流缺少 message_start", + "OpenCode SDK stream missing terminal usage": "OpenCode SDK 流缺少终止用量", + "OpenCode SDK stream must use application/x-ndjson": "OpenCode SDK 流必须使用 application/x-ndjson", + "OpenCode SDK tool input without tool start": "OpenCode SDK 工具输入缺少工具开始事件", + "OpenCode SDK tool stop reason does not match tool calls": "OpenCode SDK 工具停止原因与工具调用不匹配", + "Request body exceeds the bridge limit.": "请求正文超过桥接限制。", + "Route not found.": "找不到路由。", + "The SDK request failed. Check the selected model and provider configuration.": "SDK 请求失败。请检查所选模型和提供者配置。", + "The SDK request timed out.": "SDK 请求超时。", + "The SDK response could not be represented safely.": "无法安全表示 SDK 响应。", + "The hosted free tier is restricted for this SDK runtime.": "此 SDK 运行时使用托管免费套餐受到限制。", + "The request was canceled.": "请求已取消。", + "The selected model is unavailable in this bridge.": "此桥接中无法使用所选模型。", + "The upstream model reported that the context length was exceeded.": "上游模型报告上下文长度已超出限制。", + "The upstream provider reported exhausted quota or billing credit.": "上游提供者报告配额或计费余额已耗尽。", + "This input cannot be represented safely by the SDK.": "SDK 无法安全表示此输入。", + "This request ID is already active.": "此请求 ID 已在使用中。", + "Unsupported SDK/runtime pair or platform. Use a tested pinned runtime.": "不支持此 SDK 与运行时组合或平台。请使用经过测试的固定版本运行时。", + "a separate OpenCode bridge token is required": "需要单独的 OpenCode 桥接令牌", + "bridge URL must be a loopback HTTP(S) URL without credentials, query or fragment": "桥接 URL 必须是无凭据、查询或片段的回环 HTTP(S) URL", + "bridge URL must use localhost or a literal loopback address": "桥接 URL 必须使用 localhost 或回环 IP 地址", + "duplicate OpenCode SDK message_start": "OpenCode SDK message_start 重复", + "invalid OpenCode SDK JSON: %w": "OpenCode SDK JSON 无效:%w", + "invalid OpenCode SDK message, stop reason or usage": "OpenCode SDK 消息、停止原因或用量无效", + "invalid OpenCode SDK provider/model ID": "OpenCode SDK 提供者/模型 ID 无效", + "invalid OpenCode SDK stream JSON: %w": "OpenCode SDK 流 JSON 无效:%w", + "invalid OpenCode SDK terminal delta": "OpenCode SDK 终止增量无效", + "invalid OpenCode SDK tool arguments": "OpenCode SDK 工具参数无效", + "invalid OpenCode SDK tool input JSON": "OpenCode SDK 工具输入 JSON 无效", + "invalid OpenCode SDK trailing JSON": "OpenCode SDK 响应尾部包含无效 JSON", + "invalid OpenCode SDK usage": "OpenCode SDK 用量无效", + "invalid or unknown OpenCode SDK host tool call": "OpenCode SDK 主机工具调用无效或未知", + "local_llm_only: OpenCode SDK downstream inference cannot be restricted by the loopback bridge": "local_llm_only:回环桥接无法限制 OpenCode SDK 的下游推理连接", + "missing OpenCode SDK tool input": "缺少 OpenCode SDK 工具输入", + "unknown OpenCode SDK event type": "未知的 OpenCode SDK 事件类型", + "unsupported OpenCode SDK bridge protocol, runtime version or capture-only capabilities": "不支持此 OpenCode SDK 桥接协议、运行时版本或仅捕获功能", + "unsupported OpenCode SDK content block": "不支持此 OpenCode SDK 内容块" } diff --git a/locale/zh-Hant.json b/locale/zh-Hant.json index 371f90e2..9e5c3cd8 100644 --- a/locale/zh-Hant.json +++ b/locale/zh-Hant.json @@ -1961,5 +1961,61 @@ "🔴 Critical": "🔴 嚴重", "🔵 Low": "🔵 低危", "🟠 High": "🟠 高危", - "🟡 Medium": "🟡 中危" + "🟡 Medium": "🟡 中危", + "An unexpected additional SDK model request was blocked.": "已封鎖非預期的額外 SDK 模型請求。", + "Bridge authentication required.": "需要橋接驗證。", + "Invalid bridge provider configuration.": "橋接提供者設定無效。", + "Invalid completion request.": "補全請求無效。", + "Only non-browser loopback requests are accepted.": "僅接受非瀏覽器用戶端的迴環請求。", + "OpenCode SDK %s: %w": "OpenCode SDK %s:%w", + "OpenCode SDK HTTP %d: %w": "OpenCode SDK HTTP %d:%w", + "OpenCode SDK bridge proxy settings are not allowed": "OpenCode SDK 橋接不允許設定代理", + "OpenCode SDK event after message_stop": "OpenCode SDK 在 message_stop 之後出現事件", + "OpenCode SDK event after terminal delta": "OpenCode SDK 在終止增量之後出現事件", + "OpenCode SDK max_tokens must be nonnegative": "OpenCode SDK max_tokens 必須為非負數", + "OpenCode SDK model must be providerID/modelID": "OpenCode SDK 模型必須採用 providerID/modelID 格式", + "OpenCode SDK request exceeds 32 MiB": "OpenCode SDK 請求超過 32 MiB", + "OpenCode SDK request failed: %w": "OpenCode SDK 請求失敗:%w", + "OpenCode SDK response exceeds 32 MiB": "OpenCode SDK 回應超過 32 MiB", + "OpenCode SDK response has no usage": "OpenCode SDK 回應缺少用量", + "OpenCode SDK stream ended without message_stop": "OpenCode SDK 串流結束時缺少 message_stop", + "OpenCode SDK stream exceeds 32 MiB": "OpenCode SDK 串流超過 32 MiB", + "OpenCode SDK stream failed: %w": "OpenCode SDK 串流失敗:%w", + "OpenCode SDK stream missing message_start": "OpenCode SDK 串流缺少 message_start", + "OpenCode SDK stream missing terminal usage": "OpenCode SDK 串流缺少最終用量", + "OpenCode SDK stream must use application/x-ndjson": "OpenCode SDK 串流必須使用 application/x-ndjson", + "OpenCode SDK tool input without tool start": "OpenCode SDK 工具輸入缺少工具開始事件", + "OpenCode SDK tool stop reason does not match tool calls": "OpenCode SDK 工具停止原因與工具呼叫不符", + "Request body exceeds the bridge limit.": "請求本文超過橋接限制。", + "Route not found.": "找不到路由。", + "The SDK request failed. Check the selected model and provider configuration.": "SDK 請求失敗。請檢查所選模型及提供者設定。", + "The SDK request timed out.": "SDK 請求逾時。", + "The SDK response could not be represented safely.": "無法安全地表示 SDK 回應。", + "The hosted free tier is restricted for this SDK runtime.": "此 SDK 執行環境使用託管免費方案受到限制。", + "The request was canceled.": "請求已取消。", + "The selected model is unavailable in this bridge.": "橋接中無法使用所選模型。", + "The upstream model reported that the context length was exceeded.": "上游模型回報已超出上下文長度限制。", + "The upstream provider reported exhausted quota or billing credit.": "上游提供者回報配額或計費餘額已耗盡。", + "This input cannot be represented safely by the SDK.": "SDK 無法安全地表示此輸入。", + "This request ID is already active.": "此請求 ID 已在使用中。", + "Unsupported SDK/runtime pair or platform. Use a tested pinned runtime.": "不支援此 SDK 與執行環境組合或平台。請使用經過測試的固定版本執行環境。", + "a separate OpenCode bridge token is required": "需要獨立的 OpenCode 橋接權杖", + "bridge URL must be a loopback HTTP(S) URL without credentials, query or fragment": "橋接 URL 必須是迴環 HTTP(S) URL,且不得包含憑證、查詢或片段", + "bridge URL must use localhost or a literal loopback address": "橋接 URL 必須使用 localhost 或迴環 IP 位址字面值", + "duplicate OpenCode SDK message_start": "OpenCode SDK 的 message_start 重複", + "invalid OpenCode SDK JSON: %w": "OpenCode SDK JSON 無效:%w", + "invalid OpenCode SDK message, stop reason or usage": "OpenCode SDK 訊息、停止原因或用量無效", + "invalid OpenCode SDK provider/model ID": "OpenCode SDK 提供者/模型 ID 無效", + "invalid OpenCode SDK stream JSON: %w": "OpenCode SDK 串流 JSON 無效:%w", + "invalid OpenCode SDK terminal delta": "OpenCode SDK 終止增量無效", + "invalid OpenCode SDK tool arguments": "OpenCode SDK 工具引數無效", + "invalid OpenCode SDK tool input JSON": "OpenCode SDK 工具輸入 JSON 無效", + "invalid OpenCode SDK trailing JSON": "OpenCode SDK 回應尾端包含無效的 JSON", + "invalid OpenCode SDK usage": "OpenCode SDK 用量無效", + "invalid or unknown OpenCode SDK host tool call": "OpenCode SDK 主機工具呼叫無效或未知", + "local_llm_only: OpenCode SDK downstream inference cannot be restricted by the loopback bridge": "local_llm_only:迴環橋接無法限制 OpenCode SDK 的下游推論", + "missing OpenCode SDK tool input": "缺少 OpenCode SDK 工具輸入", + "unknown OpenCode SDK event type": "未知的 OpenCode SDK 事件類型", + "unsupported OpenCode SDK bridge protocol, runtime version or capture-only capabilities": "不支援此 OpenCode SDK 橋接通訊協定、執行環境版本或僅擷取功能", + "unsupported OpenCode SDK content block": "不支援的 OpenCode SDK 內容區塊" } diff --git a/opencodesdk/provider.go b/opencodesdk/provider.go new file mode 100644 index 00000000..8abd10ff --- /dev/null +++ b/opencodesdk/provider.go @@ -0,0 +1,596 @@ +// Package opencodesdk speaks the versioned, capture-only OpenCode bridge protocol. +// The bridge never executes host tools: Norma remains their sole executor. +package opencodesdk + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "github.com/Autumn-27/artex/locale" + "io" + "iter" + "mime" + "net/http" + "net/netip" + "net/url" + "strings" + "time" + "unicode" + + "github.com/Autumn-27/artex/llmnet" + "github.com/Autumn-27/norma/llm" + "github.com/Autumn-27/norma/transcript" + "github.com/google/uuid" +) + +const Format = "opencode-sdk" +const DefaultBaseURL = "http://127.0.0.1:4097" +const maxBody = 32 << 20 +const maxLine = 8 << 20 + +// NormalizeBaseURL accepts only explicit loopback bridges and preserves path/port identity. +func NormalizeBaseURL(raw string) (string, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + raw = DefaultBaseURL + } + u, err := url.Parse(raw) + if err != nil || u.Opaque != "" || (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" { + return "", locale.NewError("bridge URL must be a loopback HTTP(S) URL without credentials, query or fragment") + } + host := strings.ToLower(u.Hostname()) + if host != "localhost" { + ip, e := netip.ParseAddr(host) + if e != nil || !ip.IsLoopback() { + return "", locale.NewError("bridge URL must use localhost or a literal loopback address") + } + host = ip.String() + } + port := u.Port() + if (u.Scheme == "http" && port == "80") || (u.Scheme == "https" && port == "443") { + port = "" + } + if strings.Contains(host, ":") { + host = "[" + host + "]" + } + u.Host = host + if port != "" { + u.Host += ":" + port + } + u.Path = strings.TrimRight(u.Path, "/") + u.RawPath = strings.TrimRight(u.RawPath, "/") + return u.String(), nil +} + +func SameEndpoint(a, b string) bool { + aa, e := NormalizeBaseURL(a) + bb, f := NormalizeBaseURL(b) + return e == nil && f == nil && aa == bb +} + +// BridgeError retains the authenticated bridge's classification instead of guessing from diagnostic text. +type BridgeError struct { + Code string + Status int + Message string +} + +func (e *BridgeError) Error() string { + if diagnostic, ok := bridgeDiagnostics[e.Code]; ok { + // Norma's overflow classifier requires the context_length machine marker. + return e.Code + ": " + diagnostic.Error() + } + return e.Message +} + +// Only protocol-owned codes select translations; bridge-supplied prose is evidence, never a template. +func (e *BridgeError) MessageForLanguage(lang locale.Lang) string { + if diagnostic, ok := bridgeDiagnostics[e.Code]; ok { + return locale.ErrorMessage(lang, diagnostic) + } + return e.Message +} + +// Keep this bounded catalog aligned with adapters/opencode-sdk/src/protocol.mjs. +var bridgeDiagnostics = map[string]error{ + "unauthorized": locale.NewError("Bridge authentication required."), + "forbidden": locale.NewError("Only non-browser loopback requests are accepted."), + "invalid_request": locale.NewError("Invalid completion request."), + "unsupported_input": locale.NewError("This input cannot be represented safely by the SDK."), + "unsupported_runtime": locale.NewError("Unsupported SDK/runtime pair or platform. Use a tested pinned runtime."), + "unknown_model": locale.NewError("The selected model is unavailable in this bridge."), + "duplicate_request": locale.NewError("This request ID is already active."), + "canceled": locale.NewError("The request was canceled."), + "timeout": locale.NewError("The SDK request timed out."), + "sdk_failure": locale.NewError("The SDK request failed. Check the selected model and provider configuration."), + "protocol_error": locale.NewError("The SDK response could not be represented safely."), + "configuration_error": locale.NewError("Invalid bridge provider configuration."), + "second_model_request": locale.NewError("An unexpected additional SDK model request was blocked."), + "quota_exhausted": locale.NewError("The upstream provider reported exhausted quota or billing credit."), + "context_length_exceeded": locale.NewError("The upstream model reported that the context length was exceeded."), + "free_tier_restricted": locale.NewError("The hosted free tier is restricted for this SDK runtime."), + "not_found": locale.NewError("Route not found."), + "body_too_large": locale.NewError("Request body exceeds the bridge limit."), +} + +func (e *BridgeError) QuotaExhausted() bool { + return e.Code == "quota_exhausted" && (e.Status == http.StatusPaymentRequired || e.Status == 0) +} + +type Options struct { + Client *http.Client + // LocalOnly is evaluated before every operation, so cached profiles follow policy changes. + LocalOnly func() bool + ThinkingType string + ReasoningEffort string +} + +type Provider struct { + base, token, model string + client *http.Client + opts Options +} + +func New(base, token, model string, opts Options) (*Provider, error) { + normalized, err := NormalizeBaseURL(base) + if err != nil { + return nil, err + } + token = strings.TrimSpace(token) + if token == "" || strings.IndexFunc(token, unicode.IsControl) >= 0 { + return nil, locale.NewError("a separate OpenCode bridge token is required") + } + client := opts.Client + if client == nil { + client, err = llmnet.NewClient("", true, nil) + if err != nil { + return nil, err + } + } + clone := *client + clone.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } + p := &Provider{base: normalized, token: token, model: strings.TrimSpace(model), client: &clone, opts: opts} + if err := p.policy(); err != nil { + return nil, err + } + return p, nil +} + +func (p *Provider) policy() error { + if p.opts.LocalOnly != nil && p.opts.LocalOnly() { + return locale.NewError("local_llm_only: OpenCode SDK downstream inference cannot be restricted by the loopback bridge") + } + return nil +} +func (p *Provider) clean(s string) string { + s = strings.ReplaceAll(s, p.token, "[redacted]") + quoted, _ := json.Marshal(p.token) + if len(quoted) > 2 { + s = strings.ReplaceAll(s, string(quoted[1:len(quoted)-1]), "[redacted]") + } + return s +} +func (p *Provider) safeError(err error) error { + if err != nil && p.clean(err.Error()) != err.Error() { + return locale.Errorf("OpenCode SDK request failed: %w", &BridgeError{Code: "transport_error", Message: p.clean(err.Error())}) + } + return err +} +func (p *Provider) request(ctx context.Context, method, path string, body any) (*http.Response, error) { + if method != http.MethodDelete { + if err := p.policy(); err != nil { + return nil, err + } + } + var r io.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return nil, err + } + if len(b) > maxBody { + return nil, locale.NewError("OpenCode SDK request exceeds 32 MiB") + } + r = bytes.NewReader(b) + } + req, err := http.NewRequestWithContext(ctx, method, p.base+path, r) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+p.token) + req.Header.Set("Accept", "application/json") + if path == "/v1/stream" { + req.Header.Set("Accept", "application/x-ndjson") + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + resp, err := p.client.Do(req) + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + return nil, locale.Errorf("OpenCode SDK request failed: %w", &BridgeError{Code: "transport_error", Message: p.clean(err.Error())}) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + defer resp.Body.Close() + b, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10)) + var wire struct { + Error struct{ Code, Message string } `json:"error"` + } + _ = json.Unmarshal(b, &wire) + detail := wire.Error.Code + ": " + wire.Error.Message + if wire.Error.Code == "" { + detail = http.StatusText(resp.StatusCode) + } + return nil, locale.Errorf("OpenCode SDK HTTP %d: %w", resp.StatusCode, &BridgeError{Code: wire.Error.Code, Status: resp.StatusCode, Message: p.clean(detail)}) + } + return resp, nil +} + +func decode(r io.Reader, into any) error { + b, err := io.ReadAll(io.LimitReader(r, maxBody+1)) + if err != nil { + return err + } + if len(b) > maxBody { + return locale.NewError("OpenCode SDK response exceeds 32 MiB") + } + d := json.NewDecoder(bytes.NewReader(b)) + if err = d.Decode(into); err != nil { + return locale.Errorf("invalid OpenCode SDK JSON: %w", err) + } + var extra any + if err = d.Decode(&extra); err != io.EOF { + return locale.NewError("invalid OpenCode SDK trailing JSON") + } + return nil +} + +type Capabilities struct { + Protocol string `json:"protocol"` + Version int `json:"version"` + SDKVersion string `json:"sdk_version"` + Runtime string `json:"runtime"` + ToolExecution string `json:"tool_execution"` + Streaming bool `json:"streaming"` +} + +func (p *Provider) Capabilities(ctx context.Context) (Capabilities, error) { + var c Capabilities + if err := p.policy(); err != nil { + return c, p.safeError(err) + } + resp, err := p.request(ctx, "GET", "/v1/capabilities", nil) + if err != nil { + return c, p.safeError(err) + } + defer resp.Body.Close() + if err = decode(resp.Body, &c); err != nil { + return c, p.safeError(err) + } + supported := (c.Runtime == "native" && c.SDKVersion == "2.1.4") || (c.Runtime == "legacy" && c.SDKVersion == "1.19.4") + if c.Protocol != "scopeweaver-opencode-sdk" || c.Version != 1 || c.ToolExecution != "capture-only" || !c.Streaming || !supported { + return c, locale.NewError("unsupported OpenCode SDK bridge protocol, runtime version or capture-only capabilities") + } + return c, nil +} + +type Model struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// ValidModel requires an explicit provider plus a nonempty model path; nested vendor/model IDs are preserved. +func ValidModel(model string) bool { + a, b, ok := strings.Cut(model, "/") + if !ok || a == "" || b == "" || strings.IndexFunc(model, func(r rune) bool { return unicode.IsSpace(r) || unicode.IsControl(r) }) >= 0 || strings.ContainsAny(model, "?#\\") { + return false + } + for _, part := range strings.Split(b, "/") { + if part == "" { + return false + } + } + return true +} + +func (p *Provider) Models(ctx context.Context) ([]Model, error) { + if _, err := p.Capabilities(ctx); err != nil { + return nil, err + } + resp, err := p.request(ctx, "GET", "/v1/models", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + var body struct { + Models []Model `json:"models"` + } + if err = decode(resp.Body, &body); err != nil { + return nil, p.safeError(err) + } + for _, m := range body.Models { + if !ValidModel(m.ID) { + return nil, locale.NewError("invalid OpenCode SDK provider/model ID") + } + } + if body.Models == nil { + body.Models = []Model{} + } + return body.Models, nil +} + +type completionInput struct { + RequestID string `json:"request_id"` + SessionID string `json:"session_id,omitempty"` + Model string `json:"model"` + System []string `json:"system"` + Messages []llm.Message `json:"messages"` + Tools []llm.ToolSchema `json:"tools"` + MaxTokens int `json:"max_tokens"` + Temperature *float64 `json:"temperature,omitempty"` + Stop []string `json:"stop,omitempty"` + Thinking string `json:"thinking,omitempty"` + ThinkingType string `json:"thinking_type,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` +} + +func (p *Provider) input(ctx context.Context, req llm.CompletionRequest) (completionInput, error) { + if !ValidModel(p.model) { + return completionInput{}, locale.NewError("OpenCode SDK model must be providerID/modelID") + } + if req.MaxTokens < 0 { + return completionInput{}, locale.NewError("OpenCode SDK max_tokens must be nonnegative") + } + if _, err := p.Capabilities(ctx); err != nil { + return completionInput{}, err + } + tools := req.Tools + if tools == nil { + tools = []llm.ToolSchema{} + } + messages := req.Messages + if messages == nil { + messages = []llm.Message{} + } + system := req.System + if system == nil { + system = []string{} + } + maxTokens := req.MaxTokens + if maxTokens == 0 { + maxTokens = 32768 + } + return completionInput{RequestID: uuid.NewString(), SessionID: transcript.SessionIDFrom(ctx), Model: p.model, System: system, Messages: messages, Tools: tools, MaxTokens: maxTokens, Temperature: req.Temperature, Stop: req.Stop, Thinking: req.Thinking, ThinkingType: p.opts.ThinkingType, ReasoningEffort: p.opts.ReasoningEffort}, nil +} +func (p *Provider) cleanup(id string) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + resp, err := p.request(ctx, "DELETE", "/v1/requests/"+id, nil) + if err == nil { + resp.Body.Close() + } +} +func validUsage(u llm.Usage) bool { + maxInt := int(^uint(0) >> 1) + return u.InputTokens >= 0 && u.OutputTokens >= 0 && u.CacheReadTokens >= 0 && u.CacheWriteTokens >= 0 && u.CacheReadTokens <= maxInt-u.CacheWriteTokens && u.InputTokens <= maxInt-u.CacheReadTokens-u.CacheWriteTokens +} + +// Native usage excludes cache from input; Norma's canonical total includes both cached subsets. +func canonicalUsage(u llm.Usage) llm.Usage { + u.InputTokens += u.CacheReadTokens + u.CacheWriteTokens + return u +} + +func validStop(s string) bool { return s == "end_turn" || s == "tool_use" || s == "max_tokens" } +func toolNames(req llm.CompletionRequest) map[string]bool { + m := map[string]bool{} + for _, t := range req.Tools { + m[t.Name] = true + } + return m +} +func validateMessage(m llm.Message, stop string, usage llm.Usage, names map[string]bool) error { + if m.Role != llm.RoleAssistant || !validStop(stop) || !validUsage(usage) { + return locale.NewError("invalid OpenCode SDK message, stop reason or usage") + } + ids := map[string]bool{} + calls := 0 + for _, b := range m.Content { + switch b.Type { + case llm.BlockText, llm.BlockThinking: + case llm.BlockToolUse: + if b.ID == "" || ids[b.ID] || !names[b.Name] { + return locale.NewError("invalid or unknown OpenCode SDK host tool call") + } + ids[b.ID] = true + calls++ + if len(b.Input) == 0 || !json.Valid(b.Input) { + return locale.NewError("invalid OpenCode SDK tool arguments") + } + default: + return locale.NewError("unsupported OpenCode SDK content block") + } + } + if (calls > 0) != (stop == "tool_use") { + return locale.NewError("OpenCode SDK tool stop reason does not match tool calls") + } + return nil +} +func (p *Provider) Complete(ctx context.Context, req llm.CompletionRequest) (llm.Message, string, llm.Usage, error) { + in, err := p.input(ctx, req) + if err != nil { + return llm.Message{}, "", llm.Usage{}, p.safeError(err) + } + defer p.cleanup(in.RequestID) + resp, err := p.request(ctx, "POST", "/v1/complete", in) + if err != nil { + return llm.Message{}, "", llm.Usage{}, p.safeError(err) + } + defer resp.Body.Close() + var body struct { + Message llm.Message `json:"message"` + Stop string `json:"stop_reason"` + Usage *llm.Usage `json:"usage"` + } + if err = decode(resp.Body, &body); err == nil { + if body.Usage == nil { + err = locale.NewError("OpenCode SDK response has no usage") + } else { + err = validateMessage(body.Message, body.Stop, *body.Usage, toolNames(req)) + } + } + if err != nil { + return llm.Message{}, "", llm.Usage{}, p.safeError(err) + } + return body.Message, body.Stop, canonicalUsage(*body.Usage), nil +} + +type wireEvent struct { + Type llm.StreamEventType `json:"type"` + Text string `json:"text"` + ToolID string `json:"tool_id"` + ToolName string `json:"tool_name"` + StopReason string `json:"stop_reason"` + Usage *llm.Usage `json:"usage"` + Code string `json:"code"` + Message string `json:"message"` +} + +func (p *Provider) Stream(ctx context.Context, req llm.CompletionRequest) iter.Seq2[llm.StreamEvent, error] { + return func(yield func(llm.StreamEvent, error) bool) { + fail := func(e error) { yield(llm.StreamEvent{}, p.safeError(e)) } + in, err := p.input(ctx, req) + if err != nil { + fail(err) + return + } + defer p.cleanup(in.RequestID) + resp, err := p.request(ctx, "POST", "/v1/stream", in) + if err != nil { + fail(err) + return + } + defer resp.Body.Close() + media, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if media != "application/x-ndjson" { + fail(locale.NewError("OpenCode SDK stream must use application/x-ndjson")) + return + } + scanner := bufio.NewScanner(io.LimitReader(resp.Body, maxBody+1)) + scanner.Buffer(make([]byte, 64<<10), maxLine) + started, stopped, terminal, hasUsage := false, false, false, false + size := 0 + tools := map[string]bool{} + names := toolNames(req) + calls := 0 + activeTool := "" + inputs := map[string]string{} + for scanner.Scan() { + line := scanner.Bytes() + size += len(line) + 1 + if size > maxBody { + fail(locale.NewError("OpenCode SDK stream exceeds 32 MiB")) + return + } + if len(bytes.TrimSpace(line)) == 0 { + continue + } + var ev wireEvent + if err = json.Unmarshal(line, &ev); err != nil { + fail(locale.Errorf("invalid OpenCode SDK stream JSON: %w", err)) + return + } + if stopped { + fail(locale.NewError("OpenCode SDK event after message_stop")) + return + } + if ev.Type == "error" { + fail(locale.Errorf("OpenCode SDK %s: %w", p.clean(ev.Code), &BridgeError{Code: ev.Code, Message: p.clean(ev.Message)})) + return + } + if terminal && ev.Type != llm.SEMessageStop { + fail(locale.NewError("OpenCode SDK event after terminal delta")) + return + } + if !started && ev.Type != llm.SEMessageStart { + fail(locale.NewError("OpenCode SDK stream missing message_start")) + return + } + if ev.Usage != nil && !validUsage(*ev.Usage) { + fail(locale.NewError("invalid OpenCode SDK usage")) + return + } + switch ev.Type { + case llm.SEMessageStart: + if started { + fail(locale.NewError("duplicate OpenCode SDK message_start")) + return + } + started = true + case llm.SETextDelta, llm.SEThinkingDelta, llm.SEThinkingSignature: + case llm.SEToolUseStart: + if ev.ToolID == "" || tools[ev.ToolID] || !names[ev.ToolName] { + fail(locale.NewError("invalid or unknown OpenCode SDK host tool call")) + return + } + tools[ev.ToolID] = true + activeTool = ev.ToolID + calls++ + case llm.SEToolInputJSON: + if activeTool == "" { + fail(locale.NewError("OpenCode SDK tool input without tool start")) + return + } + inputs[activeTool] += ev.Text + case llm.SEMessageDelta: + if terminal || !validStop(ev.StopReason) || (calls > 0) != (ev.StopReason == "tool_use") { + fail(locale.NewError("invalid OpenCode SDK terminal delta")) + return + } + for _, input := range inputs { + if !json.Valid([]byte(input)) { + fail(locale.NewError("invalid OpenCode SDK tool input JSON")) + return + } + } + for id := range tools { + if _, ok := inputs[id]; !ok { + fail(locale.NewError("missing OpenCode SDK tool input")) + return + } + } + terminal = true + hasUsage = ev.Usage != nil + case llm.SEMessageStop: + if !terminal || !hasUsage { + fail(locale.NewError("OpenCode SDK stream missing terminal usage")) + return + } + stopped = true + default: + fail(locale.NewError("unknown OpenCode SDK event type")) + return + } + out := llm.StreamEvent{Type: ev.Type, Text: ev.Text, ToolID: ev.ToolID, ToolName: ev.ToolName, StopReason: ev.StopReason} + if ev.Usage != nil { + out.Usage = canonicalUsage(*ev.Usage) + } + if !yield(out, nil) { + return + } + } + if err = scanner.Err(); err != nil { + if ctx.Err() != nil { + err = ctx.Err() + } + fail(locale.Errorf("OpenCode SDK stream failed: %w", err)) + return + } + if !stopped { + fail(locale.NewError("OpenCode SDK stream ended without message_stop")) + } + } +} diff --git a/opencodesdk/provider_test.go b/opencodesdk/provider_test.go new file mode 100644 index 00000000..18e3a83b --- /dev/null +++ b/opencodesdk/provider_test.go @@ -0,0 +1,429 @@ +package opencodesdk + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/Autumn-27/artex/locale" + "github.com/Autumn-27/norma/compaction" + "github.com/Autumn-27/norma/llm" + "github.com/Autumn-27/norma/transcript" +) + +func TestBridgeErrorOwnedDiagnosticsLocalized(t *testing.T) { + for _, tc := range []struct { + code string + status int + want map[locale.Lang]string + }{ + {"unauthorized", 401, map[locale.Lang]string{locale.En: "Bridge authentication required.", locale.Ko: "브리지 인증이 필요합니다.", locale.ZhHans: "需要桥接身份验证。", locale.ZhHant: "需要橋接驗證。", locale.Ja: "ブリッジの認証が必要です。", locale.Es: "Se requiere autenticación del puente."}}, + {"quota_exhausted", 402, map[locale.Lang]string{locale.En: "The upstream provider reported exhausted quota or billing credit.", locale.Ko: "상위 제공자가 할당량 또는 결제 크레딧 소진을 보고했습니다.", locale.ZhHans: "上游提供者报告配额或计费余额已耗尽。", locale.ZhHant: "上游提供者回報配額或計費餘額已耗盡。", locale.Ja: "上流のプロバイダーがクォータまたは請求クレジットを使い切ったことを報告しました。", locale.Es: "El proveedor ha informado de que se ha agotado la cuota o el saldo de facturación."}}, + {"canceled", 409, map[locale.Lang]string{locale.En: "The request was canceled.", locale.Ko: "요청이 취소되었습니다.", locale.ZhHans: "请求已取消。", locale.ZhHant: "請求已取消。", locale.Ja: "リクエストがキャンセルされました。", locale.Es: "La solicitud se ha cancelado."}}, + {"context_length_exceeded", 400, map[locale.Lang]string{locale.En: "The upstream model reported that the context length was exceeded.", locale.Ko: "상위 모델이 컨텍스트 길이 초과를 보고했습니다.", locale.ZhHans: "上游模型报告上下文长度已超出限制。", locale.ZhHant: "上游模型回報已超出上下文長度限制。", locale.Ja: "上流のモデルがコンテキスト長の上限超過を報告しました。", locale.Es: "El modelo del proveedor ha informado de que se ha superado el límite de longitud del contexto."}}, + {"free_tier_restricted", 403, map[locale.Lang]string{locale.En: "The hosted free tier is restricted for this SDK runtime.", locale.Ko: "이 SDK 런타임에서는 호스팅 무료 요금제 이용이 제한됩니다.", locale.ZhHans: "此 SDK 运行时使用托管免费套餐受到限制。", locale.ZhHant: "此 SDK 執行環境使用託管免費方案受到限制。", locale.Ja: "この SDK ランタイムでは、ホスト型サービスの無料プランの利用が制限されています。", locale.Es: "El uso del nivel gratuito alojado está restringido para este entorno de ejecución del SDK."}}, + } { + for _, stream := range []bool{false, true} { + t.Run(fmt.Sprintf("%s/stream=%v", tc.code, stream), func(t *testing.T) { + p := newFixture(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method == "DELETE" { + return + } + if r.URL.Path == "/v1/capabilities" { + fmt.Fprint(w, capabilities) + return + } + if stream { + w.Header().Set("Content-Type", "application/x-ndjson") + fmt.Fprintf(w, `{"type":"error","code":%q,"message":"untrusted bridge-secret evidence"}`+"\n", tc.code) + } else { + w.WriteHeader(tc.status) + fmt.Fprintf(w, `{"error":{"code":%q,"message":"untrusted bridge-secret evidence"}}`, tc.code) + } + }) + var err error + if stream { + for _, e := range p.Stream(t.Context(), inputFixture()) { + if e != nil { + err = e + } + } + } else { + _, err = p.Models(t.Context()) + } + var bridge *BridgeError + if !errors.As(err, &bridge) || bridge.Code != tc.code || (!stream && bridge.Status != tc.status) || bridge.QuotaExhausted() != (tc.code == "quota_exhausted") { + t.Fatalf("classification lost: %#v, %v", bridge, err) + } + for lang, want := range tc.want { + got := locale.ErrorMessage(lang, err) + if !strings.Contains(got, want) || strings.Contains(got, "untrusted") || strings.Contains(got, "bridge-secret") { + t.Errorf("%s display=%q, want %q without upstream prose or credentials", lang, got, want) + } + } + }) + } + } +} + +func TestBridgeErrorContextOverflowPreservesMachineIdentity(t *testing.T) { + for code, status := range map[string]int{"context_length_exceeded": http.StatusBadRequest, "sdk_failure": http.StatusBadRequest, "free_tier_restricted": http.StatusForbidden} { + err := &BridgeError{Code: code, Status: status, Message: "arbitrary context_length_exceeded prose"} + if got := compaction.IsOverflow(err); got != (code == "context_length_exceeded") { + t.Errorf("%s: compaction overflow=%v, internal error=%q", code, got, err.Error()) + } + if code == "context_length_exceeded" && !strings.Contains(err.Error(), code) { + t.Errorf("machine marker lost: %q", err.Error()) + } + if err.QuotaExhausted() { + t.Errorf("%s incorrectly classified as quota", code) + } + } +} + +func TestBridgeErrorDiagnosticCatalogAndUnknownEvidence(t *testing.T) { + for code, diagnostic := range bridgeDiagnostics { + for _, lang := range locale.Languages() { + translated, ok := locale.Raw(lang, diagnostic.Error()) + got := locale.ErrorMessage(lang, &BridgeError{Code: code, Message: "untrusted upstream prose"}) + if !ok || translated == "" || got != translated { + t.Errorf("%s/%s missing owned diagnostic: %q", code, lang, got) + } + } + } + p := newFixture(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadGateway) + fmt.Fprint(w, `{"error":{"code":"future_upstream_code","message":"Bridge authentication required. bridge-secret 原文"}}`) + }) + _, err := p.Models(t.Context()) + var bridge *BridgeError + if !errors.As(err, &bridge) { + t.Fatal(err) + } + for _, lang := range locale.Languages() { + if got := locale.ErrorMessage(lang, bridge); got != "future_upstream_code: Bridge authentication required. [redacted] 原文" { + t.Errorf("%s unknown evidence changed or leaked credentials: %q", lang, got) + } + } +} + +const capabilities = `{"protocol":"scopeweaver-opencode-sdk","version":1,"runtime":"native","sdk_version":"2.1.4","tool_execution":"capture-only","streaming":true}` +const completeFixture = `{"message":{"role":"assistant","content":[{"type":"text","text":"Ready"},{"type":"tool_use","id":"host-call","name":"host_read","input":{"id":9223372036854775807}}]},"stop_reason":"tool_use","usage":{"input_tokens":11,"output_tokens":7,"cache_read_tokens":3,"cache_write_tokens":2}}` +const streamFixture = `{"type":"message_start"} +{"type":"text_delta","text":"Ready"} +{"type":"tool_use_start","tool_id":"host-call","tool_name":"host_read"} +{"type":"tool_input_delta","text":"{\"id\":9223372036854775807}"} +{"type":"message_delta","stop_reason":"tool_use","usage":{"input_tokens":11,"output_tokens":7,"cache_read_tokens":3,"cache_write_tokens":2}} +{"type":"message_stop"} +` + +func inputFixture() llm.CompletionRequest { + return llm.CompletionRequest{System: []string{"host system"}, Messages: []llm.Message{llm.UserText("host input")}, Tools: []llm.ToolSchema{{Name: "host_read", Description: "host-owned", InputSchema: map[string]any{"type": "object"}}}, MaxTokens: 777, Thinking: "disabled", Stop: []string{"STOP"}} +} +func newFixture(t *testing.T, handler http.HandlerFunc) *Provider { + t.Helper() + s := httptest.NewServer(handler) + t.Cleanup(s.Close) + p, e := New(s.URL, "bridge-secret", "opencode/model/with/slashes", Options{ThinkingType: "enabled", ReasoningEffort: "low"}) + if e != nil { + t.Fatal(e) + } + return p +} +func TestCompleteAndNativeStreamContract(t *testing.T) { + var mu sync.Mutex + var inputs []completionInput + var paths []string + p := newFixture(t, func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer bridge-secret" { + t.Error("wrong bridge authentication") + } + mu.Lock() + paths = append(paths, r.Method+" "+r.URL.Path) + mu.Unlock() + switch r.URL.Path { + case "/v1/capabilities": + fmt.Fprint(w, capabilities) + case "/v1/models": + fmt.Fprint(w, `{"models":[{"id":"opencode/model/with/slashes","name":"Fixture"}]}`) + case "/v1/complete", "/v1/stream": + var in completionInput + if e := json.NewDecoder(r.Body).Decode(&in); e != nil { + t.Error(e) + } + mu.Lock() + inputs = append(inputs, in) + mu.Unlock() + if strings.HasSuffix(r.URL.Path, "stream") { + w.Header().Set("Content-Type", "application/x-ndjson") + fmt.Fprint(w, streamFixture) + } else { + fmt.Fprint(w, completeFixture) + } + default: + if r.Method != "DELETE" { + t.Errorf("unexpected %s", r.URL.Path) + } + } + }) + models, e := p.Models(t.Context()) + if e != nil || len(models) != 1 || models[0].ID != "opencode/model/with/slashes" { + t.Fatalf("models=%v err=%v", models, e) + } + mu.Lock() + if len(inputs) != 0 { + t.Fatal("model discovery invoked inference") + } + mu.Unlock() + ctx := transcript.WithSessionID(t.Context(), "conv-sdk-host-session") + msg, stop, usage, e := p.Complete(ctx, inputFixture()) + if e != nil { + t.Fatal(e) + } + if msg.Text() != "Ready" || stop != "tool_use" || usage.InputTokens != 16 || usage.OutputTokens != 7 || usage.CacheReadTokens != 3 || len(msg.ToolUses()) != 1 { + t.Fatalf("complete=%+v %s %+v", msg, stop, usage) + } + var events []llm.StreamEvent + for ev, e := range p.Stream(ctx, inputFixture()) { + if e != nil { + t.Fatal(e) + } + events = append(events, ev) + } + if len(events) != 6 || events[1].Text != msg.Text() || events[2].ToolID != msg.ToolUses()[0].ID || events[3].Text != string(msg.ToolUses()[0].Input) || events[4].Usage != usage || events[4].StopReason != stop || events[5].Type != llm.SEMessageStop { + t.Fatalf("stream=%+v", events) + } + mu.Lock() + defer mu.Unlock() + if len(inputs) != 2 { + t.Fatalf("inference count=%d", len(inputs)) + } + for _, in := range inputs { + if in.RequestID == "" || in.SessionID != "conv-sdk-host-session" || in.Model != "opencode/model/with/slashes" || in.MaxTokens != 777 || in.Thinking != "disabled" || in.ThinkingType != "enabled" || in.ReasoningEffort != "low" || len(in.Tools) != 1 || in.System[0] != "host system" || in.Messages[0].Text() != "host input" { + t.Fatalf("lost request fields: %+v", in) + } + } + if inputs[0].RequestID == inputs[1].RequestID { + t.Fatal("calls share cleanup identity") + } +} +func TestEndpointValidationAndCredentialScope(t *testing.T) { + for _, raw := range []string{"https://example.com", "http://10.0.0.1", "http://127.0.0.1/?x", "http://u:p@localhost", "http://localhost/#f", "file:///tmp/bridge", "http://localhost/?"} { + if _, e := NormalizeBaseURL(raw); e == nil { + t.Errorf("accepted %q", raw) + } + } + for _, pair := range [][2]string{{"HTTP://LOCALHOST:80/bridge/", "http://localhost/bridge"}, {"http://127.0.0.1:4097/", "http://127.0.0.1:4097"}} { + if !SameEndpoint(pair[0], pair[1]) { + t.Errorf("equivalent endpoints differ: %v", pair) + } + } + for _, pair := range [][2]string{{"http://localhost", "http://127.0.0.1"}, {"http://localhost/a", "http://localhost/b"}, {"http://localhost:4097", "http://localhost:4098"}} { + if SameEndpoint(pair[0], pair[1]) { + t.Errorf("different endpoints equated: %v", pair) + } + } + for _, token := range []string{"", "x\ny"} { + if _, e := New("http://127.0.0.1", token, "p/m", Options{}); e == nil { + t.Error("accepted missing/malformed token") + } + } +} +func TestCapabilitiesBeforeInferenceAndDynamicPolicy(t *testing.T) { + var calls atomic.Int32 + var strict atomic.Bool + var capBody atomic.Value + capBody.Store(capabilities) + p := newFixture(t, func(w http.ResponseWriter, r *http.Request) { + calls.Add(1) + if r.URL.Path == "/v1/capabilities" { + fmt.Fprint(w, capBody.Load()) + } else { + fmt.Fprint(w, completeFixture) + } + }) + p.opts.LocalOnly = strict.Load + if _, e := p.Capabilities(t.Context()); e != nil { + t.Fatal(e) + } + before := calls.Load() + strict.Store(true) + if _, _, _, e := p.Complete(t.Context(), inputFixture()); e == nil || !strings.Contains(e.Error(), "local_llm_only") { + t.Fatalf("strict policy error=%v", e) + } + if calls.Load() != before { + t.Fatal("policy violation reached bridge") + } + strict.Store(false) + for _, bad := range []string{strings.Replace(capabilities, "2.1.4", "2.1.5", 1), strings.Replace(capabilities, "capture-only", "execute", 1), strings.Replace(capabilities, `"version":1`, `"version":2`, 1), `{`} { + capBody.Store(bad) + before = calls.Load() + if _, _, _, e := p.Complete(t.Context(), inputFixture()); e == nil { + t.Fatal("unsupported capabilities permitted inference") + } + if calls.Load() != before+1 { + t.Fatal("inference called after invalid capabilities") + } + } +} +func TestStreamRejectsBrokenProtocol(t *testing.T) { + cases := map[string]string{"unknown": `{"type":"surprise"}`, "missing start": `{"type":"text_delta","text":"x"}`, "malformed": `{`, "missing terminal": `{"type":"message_start"}`, "unknown tool": strings.Replace(streamFixture, "host_read", "not_offered", 1), "duplicate stop": streamFixture + "{\"type\":\"message_stop\"}\n", "missing usage": strings.Replace(streamFixture, `,"usage":{"input_tokens":11,"output_tokens":7,"cache_read_tokens":3,"cache_write_tokens":2}`, "", 1), "bad args": strings.Replace(streamFixture, `{\"id\":9223372036854775807}`, `{`, 1), "explicit error": "{\"type\":\"error\",\"code\":\"unsupported\",\"message\":\"bridge-secret\"}\n"} + for name, body := range cases { + t.Run(name, func(t *testing.T) { + p := newFixture(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v1/capabilities" { + fmt.Fprint(w, capabilities) + return + } + if r.Method == "DELETE" { + return + } + w.Header().Set("Content-Type", "application/x-ndjson") + fmt.Fprint(w, body) + }) + var got error + for _, e := range p.Stream(t.Context(), inputFixture()) { + if e != nil { + got = e + } + } + if got == nil { + t.Fatal("broken protocol succeeded") + } + if strings.Contains(got.Error(), "bridge-secret") { + t.Fatal("error leaked token") + } + }) + } +} +func TestCompleteRejectsMalformedData(t *testing.T) { + for _, body := range []string{`{}`, `{`, strings.Replace(completeFixture, "host_read", "unknown", 1), strings.Replace(completeFixture, `"output_tokens":7`, `"output_tokens":-1`, 1), completeFixture + `{}`} { + p := newFixture(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v1/capabilities" { + fmt.Fprint(w, capabilities) + } else if r.Method != "DELETE" { + fmt.Fprint(w, body) + } + }) + if _, _, _, e := p.Complete(t.Context(), inputFixture()); e == nil { + t.Fatal("malformed response succeeded") + } + } +} +func TestCancellationDeletesOnlyOwnedRequest(t *testing.T) { + for _, stream := range []bool{false, true} { + t.Run(fmt.Sprint(stream), func(t *testing.T) { + entered := make(chan string, 1) + deleted := make(chan string, 1) + p := newFixture(t, func(w http.ResponseWriter, r *http.Request) { + switch { + case r.URL.Path == "/v1/capabilities": + fmt.Fprint(w, capabilities) + case r.Method == "DELETE": + deleted <- strings.TrimPrefix(r.URL.Path, "/v1/requests/") + case r.Method == "POST": + var in completionInput + _ = json.NewDecoder(r.Body).Decode(&in) + entered <- in.RequestID + <-r.Context().Done() + } + }) + ctx, cancel := context.WithCancel(t.Context()) + done := make(chan error, 1) + go func() { + if stream { + var got error + for _, e := range p.Stream(ctx, inputFixture()) { + if e != nil { + got = e + } + } + done <- got + } else { + _, _, _, e := p.Complete(ctx, inputFixture()) + done <- e + } + }() + id := <-entered + cancel() + select { + case e := <-done: + if e == nil { + t.Fatal("cancel succeeded") + } + case <-time.After(3 * time.Second): + t.Fatal("cancel hung") + } + select { + case got := <-deleted: + if got != id { + t.Fatalf("cleanup %q want %q", got, id) + } + case <-time.After(time.Second): + t.Fatal("no explicit cancellation cleanup") + } + }) + } +} +func TestRedirectCannotLeakToken(t *testing.T) { + var leaked atomic.Int32 + foreign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { leaked.Add(1) })) + defer foreign.Close() + p := newFixture(t, func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, foreign.URL, http.StatusFound) }) + if _, e := p.Capabilities(t.Context()); e == nil || !strings.Contains(e.Error(), "302") { + t.Fatalf("redirect error=%v", e) + } + if leaked.Load() != 0 { + t.Fatal("redirect reached other endpoint") + } +} + +func TestPolicyChangeDuringCapabilityCheckBlocksInference(t *testing.T) { + var strict atomic.Bool + var posts atomic.Int32 + p := newFixture(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v1/capabilities" { + strict.Store(true) + fmt.Fprint(w, capabilities) + } else if r.Method == "POST" { + posts.Add(1) + fmt.Fprint(w, completeFixture) + } + }) + p.opts.LocalOnly = strict.Load + if _, _, _, err := p.Complete(t.Context(), inputFixture()); err == nil || !strings.Contains(err.Error(), "local_llm_only") { + t.Fatalf("policy error=%v", err) + } + if posts.Load() != 0 { + t.Fatal("inference crossed policy change after capabilities") + } +} + +func TestSupportedRuntimePairs(t *testing.T) { + for _, tc := range []struct { + runtime, version string + ok bool + }{{"native", "2.1.4", true}, {"legacy", "1.19.4", true}, {"native", "1.19.4", false}, {"legacy", "2.1.4", false}, {"native2", "2.1.4", false}, {"legacy1", "1.19.4", false}} { + t.Run(tc.runtime+"/"+tc.version, func(t *testing.T) { + body := strings.Replace(strings.Replace(capabilities, `"runtime":"native"`, `"runtime":"`+tc.runtime+`"`, 1), `"sdk_version":"2.1.4"`, `"sdk_version":"`+tc.version+`"`, 1) + p := newFixture(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/capabilities" { + t.Errorf("unexpected inference on %s", r.URL.Path) + } + fmt.Fprint(w, body) + }) + _, err := p.Capabilities(t.Context()) + if (err == nil) != tc.ok { + t.Fatalf("capabilities error=%v want supported=%v", err, tc.ok) + } + }) + } +} diff --git a/server/locale_test.go b/server/locale_test.go index b363c148..55f940bf 100644 --- a/server/locale_test.go +++ b/server/locale_test.go @@ -89,8 +89,8 @@ func TestAllProductionPackagesHaveRawShippedTranslations(t *testing.T) { } production = append(production, key) } - if len(production) != 1983 { - t.Fatalf("production key count %d, want 1983 (1982 previous + host scope error)", len(production)) + if len(production) != 2039 { + t.Fatalf("production key count %d, want 2039 (1983 previous + 56 OpenCode SDK diagnostics)", len(production)) } if err := locale.ValidateTextTranslations(production); err != nil { t.Fatal(err) diff --git a/server/opencode_sdk.go b/server/opencode_sdk.go new file mode 100644 index 00000000..0144405c --- /dev/null +++ b/server/opencode_sdk.go @@ -0,0 +1,88 @@ +package server + +import ( + "context" + "net/http" + "time" + + "github.com/Autumn-27/artex/agent" + "github.com/Autumn-27/artex/locale" + "github.com/Autumn-27/artex/opencodesdk" +) + +// Bridge tokens cannot cross formats or normalized endpoint paths, including SDK-to-SDK edits. +func sdkCredentialCompatible(cfg agent.Config, storedFormat, storedURL string) bool { + if cfg.Provider() != opencodesdk.Format && storedFormat != opencodesdk.Format { + return true + } + return cfg.Provider() == opencodesdk.Format && storedFormat == opencodesdk.Format && opencodesdk.SameEndpoint(cfg.BaseURL, storedURL) +} +func (s *Server) listOpenCodeSDKModels(w http.ResponseWriter, r *http.Request, cfg agent.Config) { + fail := func(err error) { + writeJSON(w, 200, map[string]any{"ok": false, "error": locale.ErrorMessage(responseLanguage(w), err)}) + } + client, err := cfg.HTTPClient() + if err != nil { + fail(err) + return + } + policy := func() bool { + if cfg.PolicyResolver != nil { + strict, _ := cfg.PolicyResolver() + return strict + } + return cfg.LocalLLMOnly + } + p, err := opencodesdk.New(cfg.BaseURL, cfg.APIKey, "", opencodesdk.Options{Client: client, LocalOnly: policy}) + if err != nil { + fail(err) + return + } + ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) + defer cancel() + models, err := p.Models(ctx) + if err != nil { + fail(err) + return + } + ids := make([]string, 0, len(models)) + for _, m := range models { + ids = append(ids, m.ID) + } + writeJSON(w, 200, map[string]any{"ok": true, "models": ids}) +} + +// SDK credential scope changes can make the active profile unready; never retain its old authenticated fallback. +func (s *Server) clearUnavailableSDKFallback(cfg agent.Config) { + p, err := s.m.pg.ActiveProfile() + if err != nil || p == nil { + return + } + current := agent.ConfigFrom(p.Format, p.Model, p.BaseURL, p.APIKey, p.Proxy) + if current.Ready() { + return + } + s.cfgMu.Lock() + clear := cfg.Provider() == opencodesdk.Format || s.llmCfg.Provider() == opencodesdk.Format + if clear { + s.llmOn = false + s.llmProv = nil + s.llmDirect = nil + s.chatAgent = nil + s.llmCfg = cfg + } + s.cfgMu.Unlock() + if clear { + s.invalidateTaskAgents() + } +} + +// An unavailable persisted SDK profile must not silently become a cloud environment profile on restart. +func (s *Server) environmentLLMFallback() (agent.Config, bool) { + if s.m != nil && s.m.pg != nil { + if p, err := s.m.pg.ActiveProfile(); err == nil && p != nil && p.Format == opencodesdk.Format { + return agent.Config{}, false + } + } + return agent.FromEnv() +} diff --git a/server/opencode_sdk_test.go b/server/opencode_sdk_test.go new file mode 100644 index 00000000..66382d07 --- /dev/null +++ b/server/opencode_sdk_test.go @@ -0,0 +1,374 @@ +package server + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "sync/atomic" + "testing" + + "github.com/Autumn-27/artex/agent" + "github.com/Autumn-27/artex/db" + "github.com/Autumn-27/artex/llmrec" + "github.com/Autumn-27/artex/locale" + "github.com/Autumn-27/artex/opencodesdk" + "github.com/Autumn-27/norma/llm" +) + +const sdkCaps = `{"protocol":"scopeweaver-opencode-sdk","version":1,"runtime":"native","sdk_version":"2.1.4","tool_execution":"capture-only","streaming":true}` + +func TestSDKErrorAPILocalizesOwnedBridgeDiagnostics(t *testing.T) { + t.Setenv("SCOPEWEAVER_CONFIG", filepath.Join(t.TempDir(), "missing.json")) + t.Setenv("SCOPEWEAVER_LOCAL_LLM_ONLY", "false") + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, `{"error":{"code":"unauthorized","message":"untrusted bridge-secret evidence"}}`) + })) + defer bridge.Close() + s := &Server{m: &Manager{}} + for _, h := range []http.HandlerFunc{s.testLLM, s.pgListModels} { + for lang, want := range map[locale.Lang]string{locale.En: "Bridge authentication required.", locale.Ko: "브리지 인증이 필요합니다.", locale.ZhHans: "需要桥接身份验证。", locale.ZhHant: "需要橋接驗證。", locale.Ja: "ブリッジの認証が必要です。", locale.Es: "Se requiere autenticación del puente."} { + raw, err := json.Marshal(map[string]any{"provider": opencodesdk.Format, "model": "opencode/test", "base_url": bridge.URL, "api_key": "bridge-secret", "streaming": false}) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + h(&localeWriter{rr, lang}, httptest.NewRequest("POST", "/", bytes.NewReader(raw))) + var got struct { + OK bool `json:"ok"` + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil { + t.Fatal(err) + } + if rr.Code != http.StatusOK || got.OK || !strings.Contains(got.Error, want) || strings.Contains(got.Error, "untrusted") || strings.Contains(got.Error, "bridge-secret") { + t.Errorf("%s API display=%s, want %q without upstream prose or credentials", lang, rr.Body.String(), want) + } + } + } +} + +func sdkServer(t *testing.T, token string, calls *atomic.Int32) *httptest.Server { + t.Helper() + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls.Add(1) + if r.Header.Get("Authorization") != "Bearer "+token { + t.Errorf("unscoped token=%q", r.Header.Get("Authorization")) + http.Error(w, "wrong key", 401) + return + } + switch r.URL.Path { + case "/v1/capabilities": + fmt.Fprint(w, sdkCaps) + case "/v1/models": + fmt.Fprint(w, `{"models":[{"id":"opencode/test/model","name":"Fixture"}]}`) + case "/v1/complete": + fmt.Fprint(w, `{"message":{"role":"assistant","content":[{"type":"text","text":"OK"}]},"stop_reason":"end_turn","usage":{"input_tokens":96,"output_tokens":13,"cache_read_tokens":5,"cache_write_tokens":7}}`) + default: + if r.Method != "DELETE" { + t.Errorf("unexpected inference path %s", r.URL.Path) + } + } + })) + t.Cleanup(s.Close) + return s +} +func callSDKHandler(t *testing.T, h http.HandlerFunc, body map[string]any) map[string]any { + t.Helper() + raw, e := json.Marshal(body) + if e != nil { + t.Fatal(e) + } + rr := httptest.NewRecorder() + h(rr, httptest.NewRequest("POST", "/", bytes.NewReader(raw))) + var got map[string]any + if e = json.Unmarshal(rr.Body.Bytes(), &got); e != nil || rr.Code != 200 { + t.Fatalf("HTTP %d body=%s decode=%v", rr.Code, rr.Body, e) + } + return got +} +func TestSDKDiscoveryAndConnectionScopedCredentials(t *testing.T) { + t.Setenv("SCOPEWEAVER_CONFIG", filepath.Join(t.TempDir(), "missing.json")) + t.Setenv("SCOPEWEAVER_LOCAL_LLM_ONLY", "false") + for _, source := range []string{"explicit", "same active SDK", "different active SDK", "active cloud"} { + t.Run(source, func(t *testing.T) { + var calls atomic.Int32 + bridge := sdkServer(t, "bridge-token", &calls) + s := &Server{m: &Manager{}, llmCfg: agent.ConfigFrom(opencodesdk.Format, "opencode/test", bridge.URL, "bridge-token", "")} + body := map[string]any{"provider": opencodesdk.Format, "model": "opencode/test/model", "base_url": bridge.URL, "streaming": false} + if source == "explicit" { + body["api_key"] = "bridge-token" + } else if source == "different active SDK" { + s.llmCfg.BaseURL = bridge.URL + "/other" + } else if source == "active cloud" { + s.llmCfg = agent.ConfigFrom("openai", "m", bridge.URL, "cloud-token", "") + } + got := callSDKHandler(t, s.testLLM, body) + want := source == "explicit" || source == "same active SDK" + if got["ok"] != want { + t.Fatalf("connection=%v want ok=%v", got, want) + } + if !want && calls.Load() != 0 { + t.Fatal("wrong-scope credentials reached bridge") + } + if source == "explicit" { + before := calls.Load() + got = callSDKHandler(t, s.pgListModels, body) + if got["ok"] != true || got["models"].([]any)[0] != "opencode/test/model" || calls.Load() != before+2 { + t.Fatalf("models=%v calls=%d before=%d", got, calls.Load(), before) + } + } + }) + } +} +func TestSDKStoredTokenMatchesEndpointAndFormat(t *testing.T) { + t.Setenv("SCOPEWEAVER_CONFIG", filepath.Join(t.TempDir(), "missing.json")) + t.Setenv("SCOPEWEAVER_LOCAL_LLM_ONLY", "false") + m := backportTestManager(t) + s := &Server{m: m} + var calls atomic.Int32 + bridge := sdkServer(t, "stored-token", &calls) + id, e := m.pg.SaveProfile(&db.LLMProfile{Name: "SDK-scoped-token", Format: opencodesdk.Format, BaseURL: bridge.URL, Model: "opencode/test/model", APIKey: "stored-token", Streaming: false}) + if e != nil { + t.Fatal(e) + } + defer m.pg.DeleteProfile(id) + for _, handler := range []http.HandlerFunc{s.testLLM, s.pgListModels} { + for _, scope := range []string{"same", "different path", "different format"} { + body := map[string]any{"provider": opencodesdk.Format, "base_url": bridge.URL + "/", "model": "opencode/test/model", "profile_id": id, "streaming": false} + if scope == "different path" { + body["base_url"] = bridge.URL + "/other" + } + if scope == "different format" { + body["provider"] = "openai" + body["base_url"] = "https://no-credentials.invalid" + } + before := calls.Load() + got := callSDKHandler(t, handler, body) + if scope == "same" { + if got["ok"] != true { + t.Fatalf("stored token=%v", got) + } + } else { + if got["ok"] != false || calls.Load() != before { + t.Fatalf("unscoped stored token reuse: %v calls=%d", got, calls.Load()-before) + } + } + } + } +} +func TestSDKMetadataLocalOnlyRejectedBeforeBridge(t *testing.T) { + t.Setenv("SCOPEWEAVER_CONFIG", filepath.Join(t.TempDir(), "missing.json")) + t.Setenv("SCOPEWEAVER_LOCAL_LLM_ONLY", "true") + var calls atomic.Int32 + b := sdkServer(t, "token", &calls) + s := &Server{m: &Manager{}} + got := callSDKHandler(t, s.pgListModels, map[string]any{"provider": opencodesdk.Format, "base_url": b.URL, "api_key": "token"}) + if got["ok"] != false || !strings.Contains(got["error"].(string), "local_llm_only") || calls.Load() != 0 { + t.Fatalf("policy=%v calls=%d", got, calls.Load()) + } +} + +func TestSDKUsageCanonicalLedger(t *testing.T) { + t.Setenv("SCOPEWEAVER_CONFIG", filepath.Join(t.TempDir(), "missing.json")) + t.Setenv("SCOPEWEAVER_LOCAL_LLM_ONLY", "false") + m := backportTestManager(t) + var calls atomic.Int32 + b := sdkServer(t, "token", &calls) + cfg := agent.ConfigFrom(opencodesdk.Format, "opencode/cache-fixture", b.URL, "token", "") + p, err := cfg.NewProvider() + if err != nil { + t.Fatal(err) + } + ctx := llmrec.WithTaskID(t.Context(), "sdk-cache-canonical") + defer m.pg.Exec(`DELETE FROM llm_usage WHERE task_id=$1`, "sdk-cache-canonical") + defer m.pg.Exec(`DELETE FROM llm_records WHERE task_id=$1`, "sdk-cache-canonical") + recorded := llmrec.Wrap(p, m.pg, cfg.Model, "sdk-cache-fixture", "", "", func() bool { return false }) + _, _, usage, err := recorded.Complete(ctx, llm.CompletionRequest{Messages: []llm.Message{llm.UserText("ping")}, MaxTokens: 64}) + if err != nil { + t.Fatal(err) + } + if usage.InputTokens != 108 || usage.CacheReadTokens != 5 || usage.CacheWriteTokens != 7 || usage.OutputTokens != 13 { + t.Fatalf("canonical usage=%+v", usage) + } + stats, err := m.pg.TokenByModel("sdk-cache-canonical") + if err != nil || len(stats) != 1 || stats[0].InputTokens != 108 || stats[0].CacheReadTokens != 5 || stats[0].CacheWriteTokens != 7 { + t.Fatalf("ledger=%+v err=%v", stats, err) + } +} + +func TestSDKQuotaOnlyAdvancesTaskChain(t *testing.T) { + for _, stream := range []bool{false, true} { + for _, tc := range []struct { + code string + status int + quota bool + }{{"quota_exhausted", 402, true}, {"rate_limit", 429, false}, {"sdk_failure", 503, false}, {"quota_exhausted", 401, false}, {"sdk_failure", 402, false}, {"free_tier_restricted", 403, false}} { + t.Run(fmt.Sprintf("%v/%s/%d", stream, tc.code, tc.status), func(t *testing.T) { + first := &scriptedLLMProvider{err: &opencodesdk.BridgeError{Code: tc.code, Status: tc.status, Message: "quota exceeded"}} + second := &scriptedLLMProvider{events: []llm.StreamEvent{{Type: llm.SEMessageStart}, {Type: llm.SETextDelta, Text: "backup"}, {Type: llm.SEMessageDelta, StopReason: "end_turn"}, {Type: llm.SEMessageStop}}} + active := int64(1) + rotations := 0 + hooks := taskLLMStreamHooks{current: func() (taskLLMSelection, error) { + p := first + if active == 2 { + p = second + } + return taskLLMSelection{profileID: active, provider: p, retry: agent.RetryConfig{StreamAttempts: -1}}, nil + }, exhaust: func(taskLLMSelection, error) (db.TaskLLMTransition, error) { + rotations++ + active = 2 + return db.TaskLLMTransition{Advanced: true, NextProfileID: &active}, nil + }} + var err error + if stream { + _, err = collectTaskLLMStream(streamTaskLLM(t.Context(), "sdk-quota", llm.CompletionRequest{}, hooks)) + } else { + _, _, _, err = completeTaskLLM(t.Context(), "sdk-quota", llm.CompletionRequest{}, hooks) + } + if tc.quota { + if err != nil || rotations != 1 || second.calls != 1 { + t.Fatalf("quota failover err=%v rotations=%d backup=%d", err, rotations, second.calls) + } + } else if err == nil || rotations != 0 || second.calls != 0 { + t.Fatalf("nonquota moved profile: err=%v rotations=%d backup=%d", err, rotations, second.calls) + } + }) + } + } +} +func TestSDKModelRequiredBeforeSaveAndActivation(t *testing.T) { + t.Setenv("SCOPEWEAVER_CONFIG", filepath.Join(t.TempDir(), "missing.json")) + t.Setenv("SCOPEWEAVER_LOCAL_LLM_ONLY", "false") + m := backportTestManager(t) + s := &Server{m: m} + before, err := m.pg.ListProfiles() + if err != nil { + t.Fatal(err) + } + for _, model := range []string{"", "model", "p/", "/m", "p//m"} { + raw, _ := json.Marshal(map[string]any{"name": "sdk-invalid-model", "format": opencodesdk.Format, "model": model, "base_url": "http://127.0.0.1:4097", "api_key": "token"}) + rr := httptest.NewRecorder() + s.pgSaveProfile(rr, httptest.NewRequest("POST", "/", bytes.NewReader(raw))) + if rr.Code != 400 { + t.Fatalf("invalid model %q save HTTP %d: %s", model, rr.Code, rr.Body) + } + } + after, err := m.pg.ListProfiles() + if err != nil || len(after) != len(before) { + t.Fatalf("invalid profiles stored before=%d after=%d err=%v", len(before), len(after), err) + } + id, err := m.pg.SaveProfile(&db.LLMProfile{Name: "sdk-legacy-invalid-model", Format: opencodesdk.Format, Model: "opencode/test", BaseURL: "http://127.0.0.1:4097", APIKey: "token"}) + if err != nil { + t.Fatal(err) + } + defer m.pg.DeleteProfile(id) + if _, err = m.pg.Exec(`UPDATE llm_profiles SET model='' WHERE id=$1`, id); err != nil { + t.Fatal(err) + } + activeBefore, err := m.pg.ActiveProfile() + if err != nil { + t.Fatal(err) + } + raw, _ := json.Marshal(map[string]any{"id": id}) + rr := httptest.NewRecorder() + s.pgActivateProfile(rr, httptest.NewRequest("POST", "/", bytes.NewReader(raw))) + if rr.Code != 400 { + t.Fatalf("activation HTTP %d: %s", rr.Code, rr.Body) + } + activeAfter, err := m.pg.ActiveProfile() + if err != nil { + t.Fatal(err) + } + if (activeBefore == nil) != (activeAfter == nil) || (activeBefore != nil && activeBefore.ID != activeAfter.ID) { + t.Fatal("invalid SDK model changed active profile") + } +} + +func TestSDKActiveTokenClearDisablesCachedFallback(t *testing.T) { + t.Setenv("SCOPEWEAVER_CONFIG", filepath.Join(t.TempDir(), "missing.json")) + t.Setenv("SCOPEWEAVER_LOCAL_LLM_ONLY", "false") + m := backportTestManager(t) + previous, err := m.pg.ActiveProfile() + if err != nil { + t.Fatal(err) + } + p := &db.LLMProfile{Name: "sdk-active-token-clear", Format: opencodesdk.Format, BaseURL: "http://127.0.0.1:4097/old", Model: "opencode/test", APIKey: "old-token"} + id, err := m.pg.SaveProfile(p) + if err != nil { + t.Fatal(err) + } + p.ID = id + defer func() { + if previous != nil { + _ = m.pg.SetActiveProfile(previous.ID) + } else { + _, _ = m.pg.Exec(`UPDATE llm_profiles SET is_default=false WHERE id=$1`, id) + } + _ = m.pg.DeleteProfile(id) + }() + if err = m.pg.SetActiveProfile(id); err != nil { + t.Fatal(err) + } + fake := &scriptedLLMProvider{} + s := &Server{m: m, llmOn: true, llmCfg: agent.ConfigFrom(opencodesdk.Format, p.Model, p.BaseURL, p.APIKey, ""), llmProv: fake, llmDirect: fake} + p.BaseURL = "http://127.0.0.1:4097/new" + p.APIKey = "" + if _, err = m.pg.SaveProfile(p); err != nil { + t.Fatal(err) + } + s.reapplyActiveProfile() + if _, _, ok := s.globalProvider(); ok { + t.Fatal("old SDK token remains available through cached global provider after scope change") + } + if s.llmCfg.APIKey != "" || s.llmCfg.BaseURL != p.BaseURL { + t.Fatalf("stale SDK config=%+v", s.llmCfg) + } +} + +func TestSDKUnavailableProfileBlocksCloudEnvironmentFallback(t *testing.T) { + t.Setenv("SCOPEWEAVER_CONFIG", filepath.Join(t.TempDir(), "missing.json")) + t.Setenv("SCOPEWEAVER_LOCAL_LLM_ONLY", "false") + t.Setenv("SCOPEWEAVER_LLM_PROVIDER", "openai") + t.Setenv("SCOPEWEAVER_LLM_MODEL", "environment-model") + t.Setenv("SCOPEWEAVER_LLM_API_KEY", "unrelated-cloud-key") + t.Setenv("SCOPEWEAVER_LLM_BASE_URL", "http://127.0.0.1:1") + m := backportTestManager(t) + s := &Server{m: m} + previous, err := m.pg.ActiveProfile() + if err != nil { + t.Fatal(err) + } + p := &db.LLMProfile{Name: "sdk-block-environment-fallback", Format: opencodesdk.Format, Model: "opencode/test", BaseURL: "http://127.0.0.1:4097"} + id, err := m.pg.SaveProfile(p) + if err != nil { + t.Fatal(err) + } + p.ID = id + defer func() { + if previous != nil { + _ = m.pg.SetActiveProfile(previous.ID) + } else { + _, _ = m.pg.Exec(`UPDATE llm_profiles SET is_default=false WHERE id=$1`, id) + } + _ = m.pg.DeleteProfile(id) + }() + if err = m.pg.SetActiveProfile(id); err != nil { + t.Fatal(err) + } + if _, ok := s.environmentLLMFallback(); ok { + t.Fatal("unready saved SDK profile inherited cloud environment configuration") + } + p.Format = "openai" + if _, err = m.pg.SaveProfile(p); err != nil { + t.Fatal(err) + } + cfg, ok := s.environmentLLMFallback() + if !ok || cfg.Provider() != "openai" || cfg.APIKey != "unrelated-cloud-key" { + t.Fatalf("legacy environment fallback changed: provider=%s ready=%v", cfg.Provider(), ok) + } +} diff --git a/server/server.go b/server/server.go index 936aaefb..0a6abc84 100644 --- a/server/server.go +++ b/server/server.go @@ -26,6 +26,7 @@ import ( "github.com/Autumn-27/artex/llmpool" "github.com/Autumn-27/artex/llmrec" "github.com/Autumn-27/artex/locale" + "github.com/Autumn-27/artex/opencodesdk" "github.com/Autumn-27/artex/report" "github.com/Autumn-27/artex/traffic" "github.com/Autumn-27/norma/llm" @@ -268,7 +269,7 @@ func newServer(ctx context.Context, m *Manager, skillDir string, dataDir string, } else { log.Printf("[engine] LLM configured from DB: %s / %s", cfg.Provider(), cfg.Model) } - } else if cfg, ok := agent.FromEnv(); ok { + } else if cfg, ok := s.environmentLLMFallback(); ok { if err := s.applyLLM(cfg); err != nil { log.Printf("[engine] env provider init failed — engine idle: %v", err) } else { @@ -430,6 +431,7 @@ func (s *Server) saveLLMConfig(cfg agent.Config) error { func (s *Server) reapplyActiveProfile() { cfg, ok := s.loadLLMConfig() if !ok { + s.clearUnavailableSDKFallback(cfg) return } if err := s.applyLLM(cfg); err != nil { @@ -1397,7 +1399,14 @@ func (s *Server) setLLM(w http.ResponseWriter, r *http.Request) { } if cfg.APIKey == "" && !cfg.Ready() { s.cfgMu.Lock() - cfg.APIKey = s.llmCfg.APIKey // keep existing key if not re-entered + active := s.llmCfg + if cfg.Provider() == opencodesdk.Format { + if sdkCredentialCompatible(cfg, active.Provider(), active.BaseURL) { + cfg.APIKey = active.APIKey + } + } else if active.Provider() != opencodesdk.Format { + cfg.APIKey = active.APIKey + } s.cfgMu.Unlock() } if !cfg.Ready() { @@ -1472,7 +1481,7 @@ func (s *Server) testLLM(w http.ResponseWriter, r *http.Request) { // Likewise, fall back to the stored custom session-header name when omitted by the form. if req.ProfileID != nil { if p, err := s.m.pg.ProfileByID(*req.ProfileID); err == nil && p != nil { - if cfg.APIKey == "" && (cfg.Provider() != "ollama" || p.Format == "ollama") { + if cfg.APIKey == "" && (cfg.Provider() != "ollama" || p.Format == "ollama") && sdkCredentialCompatible(cfg, p.Format, p.BaseURL) { cfg.APIKey = p.APIKey } if cfg.SessionHeaderKey == "" { @@ -1501,7 +1510,7 @@ func (s *Server) testLLM(w http.ResponseWriter, r *http.Request) { // Reuse local gateway credentials only for the same format and transport target. // Other local endpoints and native Ollama remain deliberately keyless. sameGateway := cfg.Provider() != "ollama" && cfg.Format == active.Format && cfg.BaseURL == active.BaseURL && cfg.Proxy == active.Proxy - if !cfg.Ready() || sameGateway { + if (cfg.Provider() == opencodesdk.Format && sdkCredentialCompatible(cfg, active.Provider(), active.BaseURL)) || (cfg.Provider() != opencodesdk.Format && active.Provider() != opencodesdk.Format && (!cfg.Ready() || sameGateway)) { cfg.APIKey = active.APIKey } } diff --git a/server/server_mgmt.go b/server/server_mgmt.go index 47cba894..54fcb16f 100644 --- a/server/server_mgmt.go +++ b/server/server_mgmt.go @@ -26,6 +26,7 @@ import ( "github.com/Autumn-27/artex/agent" "github.com/Autumn-27/artex/db" "github.com/Autumn-27/artex/locale" + "github.com/Autumn-27/artex/opencodesdk" "github.com/Autumn-27/norma/llm" "github.com/Autumn-27/norma/skill" ) @@ -1835,6 +1836,20 @@ func (s *Server) pgSaveProfile(w http.ResponseWriter, r *http.Request) { return } p := body.LLMProfile + if p.Format == opencodesdk.Format && !opencodesdk.ValidModel(p.Model) { + writeError(w, 400, locale.NewError("OpenCode SDK model must be providerID/modelID")) + return + } + if p.Format == opencodesdk.Format { + if _, err := opencodesdk.NormalizeBaseURL(p.BaseURL); err != nil { + writeError(w, 400, err) + return + } + if p.Proxy != "" { + writeError(w, 400, locale.NewError("OpenCode SDK bridge proxy settings are not allowed")) + return + } + } p.APIKey = body.APIKey p.Streaming = body.Streaming == nil || *body.Streaming // Negative output limits are meaningless; normalize to zero, omitting the request field. Field-name selection @@ -1961,6 +1976,17 @@ func (s *Server) pgActivateProfile(w http.ResponseWriter, r *http.Request) { writeError(w, 400, err) return } + if p, err := pg.ProfileByID(body.ID); err == nil && p != nil && p.Format == opencodesdk.Format { + if !opencodesdk.ValidModel(p.Model) { + writeError(w, 400, locale.NewError("OpenCode SDK model must be providerID/modelID")) + return + } + cfg := s.applyLLMPolicy(agent.ConfigFrom(p.Format, p.Model, p.BaseURL, p.APIKey, p.Proxy)) + if _, err = cfg.NewProvider(); err != nil { + writeError(w, 400, err) + return + } + } if err := pg.SetActiveProfile(body.ID); err != nil { writeError(w, 500, err) return @@ -2024,12 +2050,16 @@ func (s *Server) pgListModels(w http.ResponseWriter, r *http.Request) { apiKey := strings.TrimSpace(req.APIKey) if apiKey == "" && req.ProfileID != nil { if p, err := s.m.pg.ProfileByID(*req.ProfileID); err == nil && p != nil { - if req.Provider != "ollama" || p.Format == "ollama" { + if (req.Provider != "ollama" || p.Format == "ollama") && sdkCredentialCompatible(agent.ConfigFrom(req.Provider, "", req.BaseURL, "", req.Proxy), p.Format, p.BaseURL) { apiKey = p.APIKey } } } cfg := s.applyLLMPolicy(agent.ConfigFrom(req.Provider, "", req.BaseURL, apiKey, req.Proxy)) + if cfg.Provider() == opencodesdk.Format { + s.listOpenCodeSDKModels(w, r, cfg) + return + } if req.Provider == "ollama" { s.listOllamaModels(w, r, cfg) return diff --git a/server/task_llm.go b/server/task_llm.go index 043e4e15..a5a7d9d8 100644 --- a/server/task_llm.go +++ b/server/task_llm.go @@ -84,7 +84,7 @@ func isTaskLLMChainExhausted(err error) bool { // network failure, or 5xx does not rotate providers; the response must explicitly // identify quota, credits, billing balance, or payment exhaustion. func isQuotaExhaustedError(err error) bool { - return err != nil && agent.IsQuotaExhaustedMessage(err.Error()) + return agent.IsQuotaExhaustedError(err) } type taskLLMSelection struct { diff --git a/web/src/app/(main)/system/llm/page.tsx b/web/src/app/(main)/system/llm/page.tsx index fe203be9..f2da9f99 100644 --- a/web/src/app/(main)/system/llm/page.tsx +++ b/web/src/app/(main)/system/llm/page.tsx @@ -29,7 +29,7 @@ import { Separator } from "@/components/ui/separator"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { applyLLMProfilePreset, getLLMProfilePreset, isCloudEconomyPreset, isOllamaPreset, selectInstalledOllamaModel, type LLMProfilePresetId } from "@/lib/llm-profile-presets"; +import { applyLLMProfilePreset, canReuseSDKBridgeToken, getLLMProfilePreset, isCloudEconomyPreset, isOllamaPreset, selectInstalledOllamaModel, type LLMProfilePresetId } from "@/lib/llm-profile-presets"; import { api } from "@/lib/api"; import { pullCommand } from "@/lib/ollama-pull"; import type { LLMModelDetail, LLMPoolMember, LLMPoolStatus, LLMProfile, LLMRetryOverride, LLMTestResult } from "@/lib/types"; @@ -61,9 +61,10 @@ const MAX_TOKENS_FIELD_HINTS: Record = { get anthropic() { return swt("interface.m1332"); }, get "openai-responses"() { return swt("interface.m1333"); }, get ollama() { return swt("providers.maxTokensFieldOllama"); }, + get "opencode-sdk"() { return swt("providers.sdkOptionsHelp"); }, }; type ProfileFormat = LLMProfile["format"]; -const FORMATS: readonly string[] = ["anthropic", "openai", "openai-responses", "ollama"]; +const FORMATS: readonly string[] = ["anthropic", "openai", "openai-responses", "ollama", "opencode-sdk"]; const ZAI_PRESETS = ["zai-general", "zai-coding-reference"] as const; const CLOUD_PRESETS = [ { id: "deepseek", key: "providers.deepseek", help: "providers.deepseekHelp" }, @@ -424,8 +425,9 @@ function ProfileSheet({ function applyPreset(id: string) { if (!isNew) return; invalidateModels(); - const next = applyLLMProfilePreset({ name, api_key: apiKey, proxy, session_header_key: sessionHeaderKey }, id); + const next = applyLLMProfilePreset({ name, format, api_key: apiKey, proxy, session_header_key: sessionHeaderKey }, id); setPreset(id as LLMProfilePresetId); + setApiKey(next.api_key); setFormat(next.format); setModel(next.model); setBaseUrl(next.base_url); @@ -434,11 +436,13 @@ function ProfileSheet({ setCw(String(next.context_window_k)); setMaxTokens(String(next.max_tokens)); setMaxTokensField(fromStore(next.max_tokens_field)); - // A preset that defines a required session header (opencode-go) fills the field; - // presets without one keep whatever the user entered. - if (next.session_header_key) setSessionHeaderKey(next.session_header_key); + // Explicit template session settings replace the draft; absent settings are kept. + if (next.session_header_key !== undefined) setSessionHeaderKey(next.session_header_key); + if (next.format === "opencode-sdk") { + setProxy(next.proxy); + setStreaming(next.streaming); + } if (next.format === "ollama") { - setApiKey(next.api_key); setKeepAlive(next.keep_alive); setMaxConcurrent(String(next.llm_max_concurrent)); void loadModels({ format: "ollama", baseUrl: next.base_url, apiKey: "", proxy }, true); @@ -452,7 +456,10 @@ function ProfileSheet({ const profileId = profile ? Number(profile.id) : undefined; const detailOf = (m: string) => modelDetails.find((d) => (d.name ?? d.model) === m || d.model === m); // Warn only for explicit metadata: the model list or a native test reported capabilities without tools. - const keyPlaceholder = format === "ollama" ? swt("providers.keyOptional") : "sk-…"; + const isSDK = format === "opencode-sdk"; + const retainedKeyHint = isSDK || profile?.format === "opencode-sdk" + ? (canReuseSDKBridgeToken(profile, format, baseUrl) ? keyHint : "") : keyHint; + const keyPlaceholder = isSDK ? swt("providers.sdkTokenPlaceholder") : format === "ollama" ? swt("providers.keyOptional") : "sk-…"; const pull = diag && !diag.ok ? pullCommand(diag.error ?? "", model) : ""; const testedModel = diag?.ok && diag.model === model.trim() ? diag : undefined; const modelLacksTools = @@ -461,6 +468,18 @@ function ProfileSheet({ function changeFormat(nextFormat: ProfileFormat) { invalidateModels(); setFormat(nextFormat); + if (nextFormat !== format && (nextFormat === "opencode-sdk" || format === "opencode-sdk")) setApiKey(""); + if (nextFormat === "opencode-sdk" && format !== "opencode-sdk") { + setBaseUrl(getLLMProfilePreset("opencode-sdk").base_url); + setModel(""); + setProxy(""); + setSessionHeaderKey(""); + setThinkingType(NONE); + setEffort(NONE); + setMaxTokens("0"); + setMaxTokensField(NONE); + setStreaming(true); + } if (nextFormat === "ollama" && format !== "ollama") { // Drop a cloud template's URL and credential before read-only local discovery. const cloudTemplateURL = preset && !isOllamaPreset(preset) @@ -645,6 +664,10 @@ function ProfileSheet({ ))} + + {swt("providers.sdkGroup")} + {swt("providers.opencodeSdk")} +

{swt("providers.introduction")}

@@ -698,18 +721,26 @@ function ProfileSheet({ OpenAI (Chat Completions) OpenAI (Responses API) {swt("providers.ollamaFormat")} + {swt("providers.opencodeSdk")} + {isSDK && ( +
+

{swt("providers.sdkHelp")}

+

{swt("providers.sdkSecurity")}

+
+ )} +
{ invalidateModels(); setModel(e.target.value); }} /> @@ -773,6 +804,7 @@ function ProfileSheet({
{modelsError &&

{modelsError}

} + {isSDK &&

{swt("providers.sdkModelHelp")}

} {modelLacksTools && (

{swt("providers.noTools")} @@ -781,13 +813,13 @@ function ProfileSheet({

- + { invalidateModels(); setBaseUrl(e.target.value); }} + onChange={(e) => { invalidateModels(); if (isSDK) setApiKey(""); setBaseUrl(e.target.value); }} />
@@ -795,38 +827,41 @@ function ProfileSheet({ { invalidateModels(); setProxy(e.target.value); }} />

- {swt("interface.m1393")}

+ {isSDK ? swt("providers.sdkProxyHelp") : swt("interface.m1393")}

setSessionHeaderKey(e.target.value)} />

- {swt("interface.m1396")}{swt("interface.m1397")}{swt("interface.m1398")}

+ {isSDK ? swt("providers.sdkSessionHelp") : <>{swt("interface.m1396")}{swt("interface.m1397")}{swt("interface.m1398")}}

- + { invalidateModels(); setApiKey(e.target.value); }} /> {format === "ollama" &&

{swt("providers.ollamaKeyHelp")}

} + {isSDK &&

{swt("providers.sdkTokenHelp")}

}
diff --git a/web/src/i18n/i18n.test.mjs b/web/src/i18n/i18n.test.mjs index d9b16b36..486e4849 100644 --- a/web/src/i18n/i18n.test.mjs +++ b/web/src/i18n/i18n.test.mjs @@ -94,8 +94,8 @@ function checkCatalog(source, translated, locale) { } test("every shipped catalog is complete and preserves placeholders and rich tags", () => { - // 3266 shipped keys + 2 OpenCode Go provider template keys + 3 scope/asset-rule guidance keys. - assert.equal(Object.keys(catalogs.en).length, 3271); + // 3271 existing six-language keys + 12 OpenCode SDK bridge labels and guidance. + assert.equal(Object.keys(catalogs.en).length, 3283); assert.deepEqual(catalogLocales.sort(), [...LOCALES].sort()); assert.deepEqual(Object.keys(catalogs).sort(), [...LOCALES].sort()); for (const locale of LOCALES) checkCatalog(catalogs.en, catalogs[locale], locale); @@ -126,6 +126,17 @@ test("every shipped scope label and planning example preserves the exact /24 CID } }); +test("SDK bridge guidance preserves runtime, credential and model boundaries in every shipped language", () => { + for (const locale of LOCALES) { + const catalog = catalogs[locale]; + for (const version of ["2.1.4", "1.19.4", "Windows"]) { + assert.ok(catalog["providers.sdkHelp"].includes(version), `${locale}: runtime boundary ${version}`); + } + assert.ok(catalog["providers.sdkTokenHelp"].includes("SCOPEWEAVER_OPENCODE_TOKEN"), `${locale}: separate bridge credential`); + assert.ok(catalog["providers.sdkModelHelp"].includes("providerID/modelID"), `${locale}: explicit native model ID`); + } +}); + test("output-limit help states what 0 actually sends on each call path", () => { // 0 is not omitted: agent turns send 32768, direct Anthropic calls 8192, native Ollama its own default. const qualifiers = { diff --git a/web/src/i18n/messages/es.ts b/web/src/i18n/messages/es.ts index 7364e7f0..811f597c 100644 --- a/web/src/i18n/messages/es.ts +++ b/web/src/i18n/messages/es.ts @@ -5,7 +5,7 @@ export const providers: Record = { "chooseTemplate": "Elige una plantilla…", "general": "Z.ai GLM-5.3 · API general (recomendada)", "coding": "Z.ai GLM-5.3 · Coding Plan (referencia)", - "introduction": "Opcional: rellena los campos de conexión. Se conservan el nombre del perfil, el proxy, la cabecera de sesión y otras preferencias. Las plantillas de nube también conservan la clave API introducida; las de Ollama borran cualquier clave sin guardar para evitar enviar credenciales de nube a un endpoint local. Al elegir Ollama se consultan los modelos instalados; las plantillas de nube no realizan solicitudes. Elegir una plantilla no guarda ni activa un perfil. Guarda cuando esté listo y actívalo después. Todos los campos siguen siendo editables.", + introduction: "Las plantillas rellenan un perfil nuevo sin guardarlo ni activarlo. Las de nube conservan la clave introducida; las de Ollama y SDK la borran. Ollama consulta automáticamente los modelos instalados; SDK solo los consulta al pulsar Cargar modelos. SDK también borra el proxy y la cabecera de sesión. Las demás preferencias siguen siendo editables.", "generalHelp": "Usa el endpoint de la API general y su facturación, separados de la cuota de suscripción de Coding Plan. Confirma el acceso al modelo y la facturación de tu cuenta de Z.ai.", "codingHelp": "Solo como referencia: Coding Plan usa un endpoint y una cuota de suscripción independientes. Z.ai lo restringe a herramientas oficialmente compatibles; ScopeWeaver no figura entre ellas. Obtén autorización de Z.ai antes de usar este endpoint con ScopeWeaver.", "modelHelp": "GLM-5.3 solo admite texto y tiene un contexto de 1M de tokens. El razonamiento debe permanecer activado; admite los niveles low, high y max. Esta plantilla selecciona esfuerzo low, un límite de salida de 16,384 tokens y una ventana de contexto de 0 (200K por defecto). En la medición de ScopeWeaver de una tarea pequeña, low usó menos de la mitad de los tokens de max con el mismo hallazgo. Aumenta el esfuerzo para tareas más difíciles y revisa estos ajustes si cambias de modelo.", @@ -23,6 +23,18 @@ export const providers: Record = { "kimiHelp": "Valores económicos opcionales: esfuerzo de razonamiento low, límite de salida de 16,384 tokens enviado como max_completion_tokens y ventana de contexto de 0 (200K por defecto). El ahorro no se ha medido; aumenta el esfuerzo si los resultados son demasiado superficiales.", "openrouterHelp": "DeepSeek V4.1 Flash a través de OpenRouter. Esfuerzo low, límite de salida de 16,384 tokens enviado como max_tokens y ventana de contexto de 0 (200K por defecto). OpenRouter almacena automáticamente en caché los prefijos repetidos. El ahorro no se ha medido.", "opencodeGoHelp": "La pasarela de suscripción OpenCode Go (https://opencode.ai/zen/go/v1, compatible con OpenAI). Esfuerzo de razonamiento max, límite de salida de 16,384 tokens enviado como max_tokens y ventana de contexto de 0 (200K por defecto). La pasarela rechaza las solicitudes sin la cabecera x-opencode-session, por lo que esta plantilla rellena el campo Cabecera de sesión. Usa tu clave de suscripción de OpenCode (OPENCODE_API_KEY en opencode); está sujeta a las condiciones de suscripción de opencode: confirma que se permite usarla desde ScopeWeaver antes de guardar.", + opencodeSdk: "OpenCode SDK · puente", + sdkGroup: "Puente SDK", + sdkHelp: "Entornos del puente: CLI/cliente native 2.1.4 (predeterminado; conserva los argumentos originales de las herramientas) o SDK legacy 1.19.4 (JSON con estructura segura; no conserva los bytes originales). Ninguno es compatible con Windows. ScopeWeaver mantiene el control de la ejecución y aprobación de las herramientas del sistema anfitrión.", + sdkSecurity: "Un puente de bucle local también puede llamar a proveedores en la nube. Este formato no se puede usar en el modo LLM solo local. Los ID de modelos gratuitos indican su disponibilidad actual, no derechos de suscripción ni uso ilimitado.", + sdkToken: "Token del puente", + sdkTokenPlaceholder: "Token del puente obligatorio", + sdkTokenHelp: "Introduce el SCOPEWEAVER_OPENCODE_TOKEN independiente configurado en el puente, no una clave API de nube. Dejar el campo vacío conserva el token guardado solo para el mismo endpoint SDK. Editar la URL borra el token introducido; cambiar el endpoint o el formato tampoco conserva el token guardado. No se importan credenciales guardadas de proveedores.", + sdkModelHelp: "Usa el formato providerID/modelID. Cargar modelos consulta explícitamente el catálogo del puente sin realizar inferencia; elegir esta plantilla no carga modelos ni guarda o activa el perfil.", + sdkOptionsHelp: "La salida se limita al máximo declarado por el modelo nativo. Usa opciones de razonamiento compatibles con el modelo elegido; los ajustes no compatibles pueden fallar en el proveedor. Este selector del campo de salida no se aplica.", + sdkSessionHelp: "Este puente no admite cabeceras de sesión personalizadas. ScopeWeaver proporciona el historial de conversación en cada solicitud.", + sdkEndpoint: "URL del puente", + sdkProxyHelp: "No se admiten proxies del puente. Usa una URL de bucle local y configura en el puente la conexión con el proveedor de destino.", "officialModel": "Guía del modelo GLM-5.3", "officialPolicy": "Política de uso de Coding Plan", "groupCloud": "Proveedores de nube", diff --git a/web/src/i18n/messages/ja.ts b/web/src/i18n/messages/ja.ts index 0585026f..70a06af5 100644 --- a/web/src/i18n/messages/ja.ts +++ b/web/src/i18n/messages/ja.ts @@ -3375,8 +3375,7 @@ export const providers: Record = { chooseTemplate: "テンプレートを選択…", general: "Z.ai GLM-5.3 · General API(推奨)", coding: "Z.ai GLM-5.3 · Coding Plan(参考)", - introduction: - "任意で接続欄に初期値を設定できます。プロファイル名、プロキシ、セッションヘッダー、その他の設定は保持されます。クラウドのテンプレートでは入力済みの API キーも保持します。Ollama のテンプレートでは、クラウドの認証情報がローカルのエンドポイントに送信されないよう、未保存のキーを消去します。Ollama の選択時はインストール済みモデルを読み取ります。クラウドのテンプレートはリクエストを送信しません。選択してもプロファイルは保存・有効化されません。設定ができたら保存し、別途有効化してください。すべての欄は引き続き編集できます。", + introduction: "テンプレートは新規プロファイルの欄を入力するだけで、保存や有効化は行いません。クラウドのテンプレートは入力済みのキーを保持し、Ollama と SDK のテンプレートは消去します。Ollama はインストール済みモデルを自動取得しますが、SDK は「モデルを読み込む」を押したときに取得します。SDK はプロキシとセッションヘッダーの設定も消去します。他の設定は引き続き編集できます。", generalHelp: "General API のエンドポイントと API 課金を使用します。Coding Plan のサブスクリプション枠とは別です。Z.ai アカウントのモデル利用権限と課金を確認してください。", codingHelp: @@ -3404,6 +3403,18 @@ export const providers: Record = { "OpenRouter 経由の DeepSeek V4.1 Flash です。推論強度 low、max_tokens として送信する出力上限 16,384 トークン、コンテキストウィンドウ 0(既定 200K)を使用します。OpenRouter は繰り返されるプレフィックスを自動でキャッシュします。削減効果は未測定です。", opencodeGoHelp: "OpenCode Go のサブスクリプションゲートウェイ(https://opencode.ai/zen/go/v1、OpenAI 互換)です。推論強度 max、max_tokens として送信する出力上限 16,384 トークン、コンテキストウィンドウ 0(既定 200K)を使用します。ゲートウェイは x-opencode-session ヘッダーのないリクエストを拒否するため、このテンプレートが「セッションヘッダー」欄を設定します。OpenCode のサブスクリプションキー(opencode の OPENCODE_API_KEY)を使用します。このキーは opencode 独自のサブスクリプション規約に従うため、保存前に ScopeWeaver からの利用が認められることを確認してください。", + opencodeSdk: "OpenCode SDK · ブリッジ", + sdkGroup: "SDK ブリッジ", + sdkHelp: "ブリッジの実行環境は native CLI/クライアント 2.1.4(既定、ツール引数の原文を保持)または legacy SDK 1.19.4(安全な構造の JSON。元のバイト列は保持しません)です。どちらも Windows には対応していません。ホスト側のツール実行と承認は引き続き ScopeWeaver が管理します。", + sdkSecurity: "ループバックのブリッジもクラウドプロバイダーを呼び出せるため、ローカル専用 LLM モードでは使用できません。無料モデルの ID は現在の利用可能性を示すもので、サブスクリプションの利用権や無制限の利用枠を保証しません。", + sdkToken: "ブリッジトークン", + sdkTokenPlaceholder: "ブリッジトークンは必須です", + sdkTokenHelp: "クラウド API キーではなく、ブリッジに設定した専用の SCOPEWEAVER_OPENCODE_TOKEN を入力してください。空欄では同じ SDK エンドポイントの保存済みトークンだけを保持します。URL の編集で入力済みトークンを消去し、エンドポイントや形式の変更では保存済みトークンも保持しません。保存済みのプロバイダー認証情報は取り込みません。", + sdkModelHelp: "providerID/modelID 形式を使用してください。「モデルを読み込む」を押すと、推論を行わずにブリッジの一覧を取得します。テンプレートの選択だけではモデルの取得、プロファイルの保存や有効化は行いません。", + sdkOptionsHelp: "出力はネイティブモデルが宣言する上限までに制限します。選択したモデルが対応する推論オプションを使用してください。未対応の設定はプロバイダー側で失敗する場合があります。この出力フィールドの選択は適用されません。", + sdkSessionHelp: "このブリッジはカスタムセッションヘッダーに対応していません。ScopeWeaver がリクエストごとに会話履歴を渡します。", + sdkEndpoint: "ブリッジ URL", + sdkProxyHelp: "ブリッジ用のプロキシには対応していません。ループバック URL を使い、接続先プロバイダーへの接続はブリッジ側で設定してください。", officialModel: "GLM-5.3 モデルガイド", officialPolicy: "Coding Plan 利用ポリシー", groupCloud: "クラウドプロバイダー", diff --git a/web/src/i18n/messages/providers.ts b/web/src/i18n/messages/providers.ts index 0c982c92..8b5d6af4 100644 --- a/web/src/i18n/messages/providers.ts +++ b/web/src/i18n/messages/providers.ts @@ -3,7 +3,7 @@ export const en = { chooseTemplate: "Choose a template…", general: "Z.ai GLM-5.3 · General API (recommended)", coding: "Z.ai GLM-5.3 · Coding Plan (reference)", - introduction: "Optional: prefill connection fields. Your profile name, proxy, session header and other preferences are kept. Cloud templates also keep an entered API key; Ollama templates clear any unsaved key so a cloud credential is never sent to a local endpoint. Ollama selection reads installed models; cloud templates make no requests. Selection does not save or activate a profile. Save when ready, then activate separately. Every field remains editable.", + introduction: "Templates fill a new profile without saving or activating it. Cloud templates keep an entered key; Ollama and SDK templates clear it. Ollama reads installed models automatically; SDK model discovery requires Load models. SDK also clears proxy and session header settings. Other preferences remain editable.", generalHelp: "Uses the General API endpoint and API billing, separate from Coding Plan subscription quota. Confirm model access and billing for your Z.ai account.", codingHelp: "Reference only: Coding Plan uses a separate endpoint and subscription quota. Z.ai restricts it to officially supported tools; ScopeWeaver is not listed. Obtain Z.ai authorization before using this endpoint with ScopeWeaver.", modelHelp: "GLM-5.3 is text-only with a 1M-token context. Reasoning must stay enabled; supported effort levels are low, high, and max. This template selects low effort, an output limit of 16,384 tokens and context window 0 (200K default). In ScopeWeaver's measurement of one small task, low used under half the tokens of max with the same finding. Raise effort for harder tasks, and review these settings if you change the model.", @@ -21,6 +21,18 @@ export const en = { kimiHelp: "Opt-in economy defaults: reasoning effort low, output limit 16,384 tokens sent as max_completion_tokens, context window 0 (200K default). Savings are not measured; raise effort if results are too shallow.", openrouterHelp: "DeepSeek V4.1 Flash through OpenRouter. Low effort, output limit 16,384 tokens sent as max_tokens, context window 0 (200K default). OpenRouter caches repeated prefixes automatically. Savings are not measured.", opencodeGoHelp: "The OpenCode Go subscription gateway (https://opencode.ai/zen/go/v1, OpenAI-compatible). Reasoning effort max, output limit 16,384 tokens sent as max_tokens, context window 0 (200K default). The gateway rejects requests without the x-opencode-session header, so this template fills the Session header field for you. Uses your OpenCode subscription key (OPENCODE_API_KEY in opencode); the key is tied to opencode's own subscription terms — confirm that using it from ScopeWeaver is permitted before saving.", + opencodeSdk: "OpenCode SDK · bridge", + sdkGroup: "SDK bridge", + sdkHelp: "Bridge runtimes: native CLI/client 2.1.4 (default; preserves raw tool arguments) or legacy SDK 1.19.4 (safe structural JSON; original bytes are not preserved). Both are unsupported on Windows. ScopeWeaver retains host tool execution and approvals.", + sdkSecurity: "A loopback bridge can still call cloud providers. This format cannot be used with local-only LLM mode. Free model IDs reflect current availability, not subscription entitlement or unlimited use.", + sdkToken: "Bridge token", + sdkTokenPlaceholder: "Required bridge token", + sdkTokenHelp: "Enter the separate SCOPEWEAVER_OPENCODE_TOKEN configured on the bridge, not a cloud API key. A blank field keeps a saved token only for the same SDK endpoint. Changing its URL clears an entered token; changing its endpoint or format does not retain the saved token. Saved provider credentials are never imported.", + sdkModelHelp: "Use providerID/modelID. Load models explicitly reads the bridge catalog without inference; choosing this template does not load models, save or activate the profile.", + sdkOptionsHelp: "Output is capped at the native model's declared limit. Use reasoning options supported by the selected model; unsupported settings can fail at the provider. This output-field selector does not apply.", + sdkSessionHelp: "Custom session headers are not supported by this bridge. ScopeWeaver supplies the conversation history for each request.", + sdkEndpoint: "Bridge URL", + sdkProxyHelp: "Bridge proxies are not supported. Use a loopback URL and configure any downstream provider connection on the bridge.", officialModel: "GLM-5.3 model guide", officialPolicy: "Coding Plan usage policy", groupCloud: "Cloud providers", @@ -73,7 +85,7 @@ export const ko: Record = { chooseTemplate: "템플릿 선택…", general: "Z.ai GLM-5.3 · 일반 API(권장)", coding: "Z.ai GLM-5.3 · Coding Plan(참고용)", - introduction: "선택하면 연결 설정을 미리 입력합니다. 프로필 이름, 프록시, 세션 헤더와 그 밖의 설정은 유지합니다. 클라우드 템플릿은 입력한 API 키도 유지하지만, Ollama 템플릿은 저장하지 않은 키를 지워 클라우드 자격 증명이 로컬 엔드포인트로 전송되지 않게 합니다. Ollama를 선택하면 서버에 설치된 모델을 조회하며, 클라우드 템플릿은 요청을 보내지 않습니다. 선택만으로 저장하거나 활성화하지 않습니다. 준비되면 저장하고 별도로 활성화하세요. 모든 필드는 수정할 수 있습니다.", + introduction: "템플릿은 새 프로필의 입력란을 채우며 저장하거나 활성화하지 않습니다. 클라우드 템플릿은 입력한 키를 유지하고, Ollama와 SDK 템플릿은 지웁니다. Ollama는 설치된 모델을 자동 조회하지만 SDK는 모델 불러오기를 눌러야 조회합니다. SDK는 프록시와 세션 헤더 설정도 지웁니다. 그 밖의 설정은 계속 수정할 수 있습니다.", generalHelp: "일반 API 엔드포인트와 API 요금을 사용하며 Coding Plan 구독 할당량과 별개입니다. Z.ai 계정의 모델 접근 권한과 과금 조건을 확인하세요.", codingHelp: "참고용: Coding Plan은 별도 엔드포인트와 구독 할당량을 사용합니다. Z.ai가 공식 지원 도구로 사용을 제한하며 ScopeWeaver는 목록에 없습니다. ScopeWeaver에서 이 엔드포인트를 사용하려면 먼저 Z.ai의 허가를 받으세요.", modelHelp: "GLM-5.3은 텍스트 전용이며 문맥 한도는 100만 토큰입니다. 추론은 항상 켜야 하며 지원 강도는 low, high, max입니다. 템플릿은 강도 low, 출력 한도 16,384 토큰, 문맥 창 0(기본 200K)을 선택합니다. ScopeWeaver가 작은 작업 하나로 측정했을 때 low는 같은 결과를 내면서 max의 절반 미만 토큰을 썼습니다. 어려운 작업에는 강도를 올리고, 모델을 바꾸면 이 설정도 확인하세요.", @@ -91,6 +103,18 @@ export const ko: Record = { kimiHelp: "선택형 절약 기본값: 추론 강도 low, 출력 한도 16,384 토큰(max_completion_tokens로 전송), 문맥 창 0(기본 200K). 절감 효과는 측정하지 않았습니다. 결과가 얕으면 강도를 올리세요.", openrouterHelp: "OpenRouter를 통한 DeepSeek V4.1 Flash입니다. 강도 low, 출력 한도 16,384 토큰(max_tokens로 전송), 문맥 창 0(기본 200K). OpenRouter는 반복되는 앞부분을 자동으로 캐시합니다. 절감 효과는 측정하지 않았습니다.", opencodeGoHelp: "OpenCode Go 구독 게이트웨이(https://opencode.ai/zen/go/v1, OpenAI 호환)입니다. 추론 강도 max, 출력 한도 16,384 토큰(max_tokens로 전송), 문맥 창 0(기본 200K). 게이트웨이는 x-opencode-session 헤더가 없는 요청을 거부하므로 이 템플릿이 세션 헤더 필드를 채워 줍니다. opencode의 구독 키(OPENCODE_API_KEY)를 사용합니다. 키는 opencode 구독 약관에 묶여 있으므로 ScopeWeaver에서 사용해도 되는지 먼저 확인하세요.", + opencodeSdk: "OpenCode SDK · 브리지", + sdkGroup: "SDK 브리지", + sdkHelp: "브리지 런타임은 native CLI·클라이언트 2.1.4(기본값, 도구 인자 원본 보존) 또는 legacy SDK 1.19.4(안전한 구조의 JSON, 원본 바이트는 보존하지 않음)입니다. 둘 다 Windows는 지원하지 않습니다. 호스트 도구 실행과 승인은 ScopeWeaver가 관리합니다.", + sdkSecurity: "루프백 브리지도 클라우드 제공자를 호출할 수 있어 로컬 전용 LLM 모드에서는 사용할 수 없습니다. 무료 모델 ID는 현재 이용 가능 여부를 나타낼 뿐 구독 사용 권한이나 무제한 이용을 보장하지 않습니다.", + sdkToken: "브리지 토큰", + sdkTokenPlaceholder: "필수 브리지 토큰", + sdkTokenHelp: "클라우드 API 키 대신 브리지에 설정한 별도의 SCOPEWEAVER_OPENCODE_TOKEN을 입력하세요. 빈칸은 같은 SDK 엔드포인트의 저장된 토큰만 유지합니다. URL을 수정하면 입력한 토큰을 지우며, 엔드포인트나 형식이 바뀌면 저장된 토큰도 유지하지 않습니다. 저장된 제공자 자격 증명은 가져오지 않습니다.", + sdkModelHelp: "providerID/modelID 형식을 사용하세요. 모델 불러오기를 눌러야 추론 없이 브리지 목록을 조회합니다. 템플릿 선택만으로 모델을 조회하거나 프로필을 저장·활성화하지 않습니다.", + sdkOptionsHelp: "출력은 네이티브 모델에 선언된 한도 이내로 제한합니다. 선택한 모델이 지원하는 추론 옵션을 사용하세요. 지원하지 않는 설정은 제공자에서 실패할 수 있습니다. 출력 필드 선택은 적용되지 않습니다.", + sdkSessionHelp: "이 브리지는 사용자 지정 세션 헤더를 지원하지 않습니다. ScopeWeaver가 요청마다 대화 이력을 전달합니다.", + sdkEndpoint: "브리지 URL", + sdkProxyHelp: "브리지 프록시는 지원하지 않습니다. 루프백 URL을 사용하고, 하위 제공자의 연결 설정은 브리지에서 구성하세요.", officialModel: "GLM-5.3 모델 안내", officialPolicy: "Coding Plan 이용 정책", groupCloud: "클라우드 제공자", diff --git a/web/src/i18n/messages/zh-hans.ts b/web/src/i18n/messages/zh-hans.ts index ed6bf556..9841c42a 100644 --- a/web/src/i18n/messages/zh-hans.ts +++ b/web/src/i18n/messages/zh-hans.ts @@ -3278,8 +3278,7 @@ export const providers: Record = { chooseTemplate: "选择模板…", general: "Z.ai GLM-5.3 · 通用 API(推荐)", coding: "Z.ai GLM-5.3 · Coding Plan(参考)", - introduction: - "可选:预填连接字段。配置名称、代理、会话标头和其他偏好会保留。云端模板还会保留已输入的 API 密钥;Ollama 模板会清除未保存的密钥,避免云端凭据被发送到本地端点。选择 Ollama 会读取服务器上已安装的模型;云端模板不会发送请求。选择模板不会保存或启用配置。准备好后请保存,再单独启用。所有字段仍可编辑。", + introduction: "模板只填充新配置,不会保存或激活。云端模板保留已输入的密钥;Ollama 和 SDK 模板会清除密钥。Ollama 自动查询已安装模型;SDK 需要点击“加载模型”才查询。SDK 还会清除代理和会话请求头设置。其他设置仍可编辑。", generalHelp: "使用通用 API 端点并按 API 计费,与 Coding Plan 订阅额度相互独立。请确认 Z.ai 账号的模型权限和计费方式。", codingHelp: @@ -3307,6 +3306,18 @@ export const providers: Record = { "通过 OpenRouter 使用 DeepSeek V4.1 Flash。强度 low,输出上限 16,384 token(以 max_tokens 发送),上下文窗口 0(默认 200K)。OpenRouter 会自动缓存重复的前缀。节省效果未经测量。", opencodeGoHelp: "OpenCode Go 订阅网关(https://opencode.ai/zen/go/v1,OpenAI 兼容)。推理强度 max,输出上限 16,384 token(以 max_tokens 发送),上下文窗口 0(默认 200K)。网关会拒绝缺少 x-opencode-session 标头的请求,因此该模板会自动填写“会话标头”字段。使用你的 OpenCode 订阅密钥(opencode 中的 OPENCODE_API_KEY)。该密钥受 opencode 订阅条款约束,保存前请确认允许在 ScopeWeaver 中使用。", + opencodeSdk: "OpenCode SDK · 桥接服务", + sdkGroup: "SDK 桥接服务", + sdkHelp: "桥接运行时:native CLI/客户端 2.1.4(默认,保留原始工具参数)或 legacy SDK 1.19.4(安全的结构化 JSON,不保留原始字节)。两者均不支持 Windows。主机工具执行和审批仍由 ScopeWeaver 管理。", + sdkSecurity: "回环桥接服务仍可能调用云端提供商,因此不能用于仅限本地 LLM 模式。免费模型 ID 只表示当前可用性,不代表订阅使用权或无限使用额度。", + sdkToken: "桥接令牌", + sdkTokenPlaceholder: "必填桥接令牌", + sdkTokenHelp: "请输入桥接服务配置的独立 SCOPEWEAVER_OPENCODE_TOKEN,而非云端 API 密钥。留空仅保留同一 SDK 端点已保存的令牌。修改 URL 会清除已输入的令牌;更换端点或格式不会保留已保存的令牌。不会导入已保存的提供商凭据。", + sdkModelHelp: "使用 providerID/modelID 格式。点击“加载模型”才会读取桥接目录,不执行推理。选择模板不会加载模型、保存或激活配置。", + sdkOptionsHelp: "输出受原生模型声明的上限约束。请使用所选模型支持的推理选项;不支持的设置可能在提供商端失败。此输出字段选择器不适用。", + sdkSessionHelp: "此桥接服务不支持自定义会话请求头。ScopeWeaver 会为每次请求提供对话历史。", + sdkEndpoint: "桥接 URL", + sdkProxyHelp: "不支持桥接代理。请使用回环 URL,并在桥接服务上配置下游提供商连接。", officialModel: "GLM-5.3 模型指南", officialPolicy: "Coding Plan 使用政策", groupCloud: "云端服务商", diff --git a/web/src/i18n/messages/zh-hant.ts b/web/src/i18n/messages/zh-hant.ts index 70c1598b..90689691 100644 --- a/web/src/i18n/messages/zh-hant.ts +++ b/web/src/i18n/messages/zh-hant.ts @@ -3278,8 +3278,7 @@ export const providers: Record = { chooseTemplate: "選擇模板…", general: "Z.ai GLM-5.3 · 通用 API(推薦)", coding: "Z.ai GLM-5.3 · Coding Plan(參考)", - introduction: - "可選:預填連線欄位。設定名稱、代理、工作階段標頭和其他偏好會保留。雲端模板還會保留已輸入的 API 金鑰;Ollama 模板會清除未儲存的金鑰,避免雲端憑據被髮送到本地端點。選擇 Ollama 會讀取伺服器上已安裝的模型;雲端模板不會傳送請求。選擇模板不會儲存或啟用設定。準備好後請儲存,再單獨啟用。所有欄位仍可編輯。", + introduction: "模板僅填入新設定,不會儲存或啟用。雲端模板保留已輸入的金鑰;Ollama 和 SDK 模板會清除金鑰。Ollama 自動查詢已安裝的模型;SDK 需按下「載入模型」才會查詢。SDK 也會清除代理與工作階段標頭設定。其他設定仍可編輯。", generalHelp: "使用通用 API 端點並按 API 計費,與 Coding Plan 訂閱額度相互獨立。請確認 Z.ai 帳號的模型權限和計費方式。", codingHelp: @@ -3307,6 +3306,18 @@ export const providers: Record = { "透過 OpenRouter 使用 DeepSeek V4.1 Flash。強度 low,輸出上限 16,384 token(以 max_tokens 傳送),上下文視窗 0(預設 200K)。OpenRouter 會自動快取重複的字首。節省效果未經測量。", opencodeGoHelp: "OpenCode Go 訂閱閘道器(https://opencode.ai/zen/go/v1,OpenAI 相容)。推理強度 max,輸出上限 16,384 token(以 max_tokens 傳送),上下文視窗 0(預設 200K)。閘道器會拒絕缺少 x-opencode-session 標頭的請求,因此該模板會自動填寫“工作階段標頭”欄位。使用你的 OpenCode 訂閱金鑰(opencode 中的 OPENCODE_API_KEY)。該金鑰受 opencode 訂閱條款約束,儲存前請確認允許在 ScopeWeaver 中使用。", + opencodeSdk: "OpenCode SDK · 橋接服務", + sdkGroup: "SDK 橋接服務", + sdkHelp: "橋接執行階段:native CLI/用戶端 2.1.4(預設,保留原始工具引數)或 legacy SDK 1.19.4(安全的結構化 JSON,不保留原始位元組)。兩者均不支援 Windows。主機工具執行與核准仍由 ScopeWeaver 管理。", + sdkSecurity: "回環橋接服務仍可能呼叫雲端提供者,因此無法用於僅限本機 LLM 模式。免費模型 ID 僅表示目前可用,不代表訂閱使用權或無限使用額度。", + sdkToken: "橋接權杖", + sdkTokenPlaceholder: "必填橋接權杖", + sdkTokenHelp: "請輸入橋接服務設定的獨立 SCOPEWEAVER_OPENCODE_TOKEN,而非雲端 API 金鑰。留白僅保留同一 SDK 端點已儲存的權杖。修改 URL 會清除已輸入的權杖;變更端點或格式不會保留已儲存的權杖。不會匯入已儲存的提供者憑證。", + sdkModelHelp: "使用 providerID/modelID 格式。按下「載入模型」才會讀取橋接目錄,不執行推論。選擇模板不會載入模型、儲存或啟用設定。", + sdkOptionsHelp: "輸出受原生模型宣告的上限約束。請使用所選模型支援的推理選項;不支援的設定可能在提供者端失敗。此輸出欄位選擇器不適用。", + sdkSessionHelp: "此橋接服務不支援自訂工作階段標頭。ScopeWeaver 會為每次請求提供對話歷程。", + sdkEndpoint: "橋接 URL", + sdkProxyHelp: "不支援橋接代理。請使用回環 URL,並在橋接服務上設定下游提供者連線。", officialModel: "GLM-5.3 模型指南", officialPolicy: "Coding Plan 使用政策", groupCloud: "雲端服務商", diff --git a/web/src/lib/llm-profile-presets.test.mjs b/web/src/lib/llm-profile-presets.test.mjs index b749bd0f..0fc9eed0 100644 --- a/web/src/lib/llm-profile-presets.test.mjs +++ b/web/src/lib/llm-profile-presets.test.mjs @@ -1,6 +1,37 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { applyLLMProfilePreset, getLLMProfilePreset, isOllamaPreset, selectInstalledOllamaModel, LLM_PROFILE_PRESET_IDS } from "./llm-profile-presets.ts"; +import { applyLLMProfilePreset, canReuseSDKBridgeToken, getLLMProfilePreset, isOllamaPreset, selectInstalledOllamaModel, LLM_PROFILE_PRESET_IDS } from "./llm-profile-presets.ts"; + +test("OpenCode SDK template isolates bridge credentials and waits for explicit model discovery", () => { + const draft = Object.freeze({ name: "SDK", api_key: "unsaved-cloud-key", proxy: "http://proxy:8080", session_header_key: "x-opencode-session", streaming: false, rate_per_minute: 7 }); + const next = applyLLMProfilePreset(draft, "opencode-sdk"); + assert.deepEqual(getLLMProfilePreset("opencode-sdk"), { + format: "opencode-sdk", base_url: "http://127.0.0.1:4097", model: "", api_key: "", proxy: "", session_header_key: "", streaming: true, + thinking_type: "", reasoning_effort: "", context_window_k: 0, max_tokens: 0, max_tokens_field: "", + }); + assert.equal(next.name, draft.name); + assert.equal(next.rate_per_minute, draft.rate_per_minute); + for (const field of ["api_key", "proxy", "session_header_key", "model"]) assert.equal(next[field], "", field); + assert.equal(next.streaming, true); + assert.equal(draft.api_key, "unsaved-cloud-key"); + assert.equal(isOllamaPreset("opencode-sdk"), false, "SDK discovery must not use automatic Ollama discovery"); + assert.equal(applyLLMProfilePreset(next, "openai").api_key, "", "leaving SDK must not invent a cloud key"); + assert.equal(applyLLMProfilePreset({ ...next, api_key: "bridge-token" }, "openai").api_key, "", "switching templates must not send a bridge token to a cloud provider"); +}); + +test("saved bridge-token hints require the same SDK endpoint and never cross host aliases or formats", () => { + const profile = { format: "opencode-sdk", base_url: "http://127.0.0.1:4097/bridge/" }; + assert.equal(canReuseSDKBridgeToken(profile, "opencode-sdk", " http://127.0.0.1:4097/bridge "), true); + for (const url of ["http://localhost:4097/bridge", "http://[::1]:4097/bridge", "http://2130706433:4097/bridge", "http://127.0.0.1:4098/bridge", "https://127.0.0.1:4097/bridge", "http://127.0.0.1:4097/other", "http://127.0.0.1:4097/bridge/a/../", "http://127.0.0.1:4097/bridge?token=x", "http://127.0.0.1:4097/bridge#x", "http://user@127.0.0.1:4097/bridge", "invalid", ""]) { + assert.equal(canReuseSDKBridgeToken(profile, "opencode-sdk", url), false, url); + } + for (const format of ["openai", "anthropic", "openai-responses", "ollama"]) { + assert.equal(canReuseSDKBridgeToken(profile, format, profile.base_url), false, format); + assert.equal(canReuseSDKBridgeToken({ ...profile, format }, "opencode-sdk", profile.base_url), false, format); + } + assert.equal(canReuseSDKBridgeToken(null, "opencode-sdk", profile.base_url), false); + assert.equal(canReuseSDKBridgeToken({ format: "opencode-sdk", base_url: "" }, "opencode-sdk", "http://127.0.0.1:4097/"), true, "blank SDK URLs use the default bridge root"); +}); test("GLM templates distinguish billing endpoints and use the measured economy settings", () => { const general = getLLMProfilePreset("zai-general"); diff --git a/web/src/lib/llm-profile-presets.ts b/web/src/lib/llm-profile-presets.ts index 0ab7140e..1cf2647c 100644 --- a/web/src/lib/llm-profile-presets.ts +++ b/web/src/lib/llm-profile-presets.ts @@ -14,6 +14,7 @@ export const LLM_PROFILE_PRESET_IDS = [ "kimi", "openrouter", "opencode-go", + "opencode-sdk", "ollama-host", "ollama-docker-host", "ollama-compose", @@ -70,7 +71,21 @@ export interface OllamaPresetFields { session_header_key?: string; } -export type LLMProfilePresetFields = ZaiPresetFields | CloudPresetFields | OllamaPresetFields; +export interface OpenCodeSDKPresetFields { + format: "opencode-sdk"; + model: ""; + base_url: string; + api_key: ""; + proxy: ""; + session_header_key: ""; + streaming: true; + thinking_type: ""; + reasoning_effort: ""; + context_window_k: number; + max_tokens: number; + max_tokens_field: ""; +} +export type LLMProfilePresetFields = ZaiPresetFields | CloudPresetFields | OllamaPresetFields | OpenCodeSDKPresetFields; const OLLAMA_TARGETS: Record = { "ollama-host": { base_url: "http://127.0.0.1:11434" }, @@ -110,6 +125,12 @@ export function getLLMProfilePreset(id: string): Readonly(draft: T, id: string): T & LLMProfilePresetFields { - return { ...draft, ...getLLMProfilePreset(id) }; + const preset = getLLMProfilePreset(id); + if ("format" in draft && draft.format === "opencode-sdk" && preset.format !== "opencode-sdk") { + return { ...draft, ...preset, api_key: "" }; + } + return { ...draft, ...preset }; +} + +// A saved bridge token belongs only to that SDK endpoint, never to another format or host alias. +export function canReuseSDKBridgeToken( + profile: { format: string; base_url?: string } | null | undefined, + format: string, + baseUrl: string, +): boolean { + if (profile?.format !== "opencode-sdk" || format !== "opencode-sdk") return false; + const endpoint = (value: string) => { + const raw = value.trim() || "http://127.0.0.1:4097"; + const parts = /^https?:\/\/(\[[^\]]+\]|[^/:?#]+)(?::\d+)?(\/[^?#]*)?$/.exec(raw); + if (!parts || raw.includes("\\")) return ""; + const url = new URL(raw); + if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) return ""; + const host = parts[1].toLowerCase(); + if (!host.startsWith("[") && host !== url.hostname) return ""; + if (url.hostname !== "localhost" && url.hostname !== "[::1]" && !/^127\./.test(url.hostname)) return ""; + // Preserve path identity: the Go endpoint normalizer does not resolve dot segments. + return url.origin + (parts[2] ?? "").replace(/\/+$/, ""); + }; + try { + const previous = endpoint(profile.base_url ?? ""); + return previous !== "" && previous === endpoint(baseUrl); + } catch { + return false; + } } // Choose only from the installed list, preferring confirmed native tool support. diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 92fd96bd..8dc65008 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -1044,7 +1044,7 @@ export interface NotificationDelivery { export interface LLMProfile { id: string; name: string; - format: "openai" | "anthropic" | "openai-responses" | "ollama"; + format: "openai" | "anthropic" | "openai-responses" | "ollama" | "opencode-sdk"; base_url?: string; proxy?: string; model: string; From 29b6addb302493837db4883a1c11fb6eef4fce8f Mon Sep 17 00:00:00 2001 From: cskwork Date: Sun, 25 Oct 2026 22:06:55 +0900 Subject: [PATCH 2/6] fix(llm): honor SDK connection-test output limits --- agent/provider.go | 6 ++- agent/provider_sdk_test.go | 65 +++++++++++++++++++++++++ docs/es/llm-providers.md | 2 +- docs/ja/llm-providers.md | 2 +- docs/ko/llm-providers.md | 2 +- docs/llm-providers.md | 2 +- docs/zh-Hans/llm-providers.md | 2 +- docs/zh-Hant/llm-providers.md | 2 +- server/opencode_sdk_test.go | 91 +++++++++++++++++++++++++++++++++++ server/server.go | 3 ++ 10 files changed, 170 insertions(+), 7 deletions(-) diff --git a/agent/provider.go b/agent/provider.go index f141956e..6089d8c1 100644 --- a/agent/provider.go +++ b/agent/provider.go @@ -645,12 +645,16 @@ func TestConnection(ctx context.Context, c Config) (time.Duration, string, error // yielding finish=length and confusing interruption/resume indications despite // transport success. A sufficient budget lets the model emit OK and stop cleanly. // Keep EscalateMaxTokens false to avoid wasteful retry/resume loops after truncation. + maxTokens := 8192 + if c.Provider() == opencodesdk.Format && c.MaxTokens > 0 { + maxTokens = c.MaxTokens + } reply, err := agentcore.Run(ctx, agentcore.Options{ Provider: prov, SystemPrompt: []string{locale.Text(locale.FromContext(ctx), "This is a connection test. Output exactly the two characters OK, with no reasoning, explanation, or other content.")}, PermissionMode: acperm.ModeBypass, MaxTurns: 1, - MaxTokens: 8192, + MaxTokens: maxTokens, NonStreaming: !c.Stream, // Test using this profile's actual streaming/non-streaming mode. }, "ping") lat := time.Since(start) diff --git a/agent/provider_sdk_test.go b/agent/provider_sdk_test.go index 0b907782..d4c8811f 100644 --- a/agent/provider_sdk_test.go +++ b/agent/provider_sdk_test.go @@ -24,6 +24,71 @@ func TestConfigFromOpenCodeSDK(t *testing.T) { t.Fatal("SDK must require its bridge token even on loopback") } } + +func TestSDKConnectionHonorsConfiguredTokenLimit(t *testing.T) { + for _, stream := range []bool{false, true} { + for _, tc := range []struct { + name, provider string + configured, want int + }{ + {"SDK configured", opencodesdk.Format, 1024, 1024}, + {"SDK unspecified", opencodesdk.Format, 0, 8192}, + {"SDK nonpositive", opencodesdk.Format, -1, 8192}, + {"OpenAI default unchanged", "openai", 1024, 8192}, + } { + t.Run(fmt.Sprintf("%s/stream=%v", tc.name, stream), func(t *testing.T) { + var calls, maxTokens atomic.Int32 + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer bridge-token" { + t.Error("connection request did not use its dedicated token") + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + if r.URL.Path == "/v1/capabilities" { + fmt.Fprint(w, `{"protocol":"scopeweaver-opencode-sdk","version":1,"runtime":"native","sdk_version":"2.1.4","tool_execution":"capture-only","streaming":true}`) + return + } + if r.Method == http.MethodDelete { + return + } + var body struct { + MaxTokens int `json:"max_tokens"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Error(err) + http.Error(w, "invalid request", http.StatusBadRequest) + return + } + calls.Add(1) + maxTokens.Store(int32(body.MaxTokens)) + if tc.provider == opencodesdk.Format { + if stream { + w.Header().Set("Content-Type", "application/x-ndjson") + fmt.Fprintln(w, `{"type":"message_start"}`) + fmt.Fprintln(w, `{"type":"text_delta","text":"OK"}`) + fmt.Fprintln(w, `{"type":"message_delta","stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}`) + fmt.Fprintln(w, `{"type":"message_stop"}`) + } else { + fmt.Fprint(w, `{"message":{"role":"assistant","content":[{"type":"text","text":"OK"}]},"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}`) + } + } else if stream { + w.Header().Set("Content-Type", "text/event-stream") + fmt.Fprint(w, "data: {\"choices\":[{\"delta\":{\"content\":\"OK\"},\"finish_reason\":null}]}\n\ndata: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":1}}\n\ndata: [DONE]\n\n") + } else { + fmt.Fprint(w, `{"choices":[{"message":{"role":"assistant","content":"OK"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1}}`) + } + })) + defer bridge.Close() + cfg := ConfigFrom(tc.provider, "fixture/model", bridge.URL, "bridge-token", "") + cfg.Stream, cfg.MaxTokens = stream, tc.configured + _, reply, err := TestConnection(t.Context(), cfg) + if err != nil || reply != "OK" || calls.Load() != 1 || int(maxTokens.Load()) != tc.want { + t.Fatalf("reply=%q error=%v inference calls=%d max_tokens=%d want 1/%d", reply, err, calls.Load(), maxTokens.Load(), tc.want) + } + }) + } + } +} func TestFromEnvOpenCodeSDKTokenIsolation(t *testing.T) { for _, tc := range []struct { name, modern, dedicated string diff --git a/docs/es/llm-providers.md b/docs/es/llm-providers.md index ebae4986..e725d3ba 100644 --- a/docs/es/llm-providers.md +++ b/docs/es/llm-providers.md @@ -77,7 +77,7 @@ Conserva el token para el campo **Token del puente**. El entorno predeterminado La plantilla borra claves de nube sin guardar, deja el modelo vacío y **no descubre, infiere, guarda ni activa**. Introduce el token y pulsa **Cargar modelos** para consultar el catálogo sin inferencia. Guarda y activa por separado cuando esté listo. **Probar conexión** solo hace una solicitud al modelo al pulsarlo; descubrir metadatos no demuestra acceso de inferencia. -**Verificación gratuita anterior — 2026-10-25 KST:** si figura en la lista, selecciona `opencode/deepseek-v4.1-flash-free`. La ejecución anterior sin iniciar sesión en macOS arm64, con CLI auténtico 2.1.4, superó conexión de interfaz, escritura Bash del anfitrión tras aprobación ASK manual, archivo con `FREE_OK` exacto más salto de línea y respuesta final. Hubo **3 POST de inferencia**, salida máxima **1,024**, reintentos desactivados, sin modelo de pago alternativo ni ejecución nativa observada. Solo demuestra un pequeño flujo aislado de esa fecha, no una repetición actual, disponibilidad permanente ni uso ilimitado. Verifica disponibilidad, elegibilidad y límites actuales. El acceso gratuito alojado heredado sigue sin verificarse. +**Verificación gratuita anterior — 2026-10-25 KST:** si figura en la lista, selecciona `opencode/deepseek-v4.1-flash-free`. La ejecución anterior sin iniciar sesión en macOS arm64, con CLI auténtico 2.1.4, superó conexión de interfaz, escritura Bash del anfitrión tras aprobación ASK manual, archivo con `FREE_OK` exacto más salto de línea y respuesta final. Hubo **3 POST de inferencia**, reintentos desactivados, sin modelo de pago alternativo ni ejecución nativa observada. Solo demuestra un pequeño flujo aislado de esa fecha, no una repetición actual, disponibilidad permanente ni uso ilimitado. Verifica disponibilidad, elegibilidad y límites actuales. El acceso gratuito alojado heredado sigue sin verificarse. Al editar, un token vacío conserva el guardado solo en el mismo endpoint SDK. Cambiar URL borra el token introducido; cambiar endpoint o formato con token vacío no reutiliza credenciales. `localhost`, `127.0.0.1` y `::1` son ámbitos distintos. ID y límites de salida proceden de metadatos actuales. El razonamiento depende del proveedor/modelo y falla explícitamente si no es compatible. diff --git a/docs/ja/llm-providers.md b/docs/ja/llm-providers.md index 4fb81bfc..6b2745e4 100644 --- a/docs/ja/llm-providers.md +++ b/docs/ja/llm-providers.md @@ -82,7 +82,7 @@ bun run start テンプレートは未保存のクラウドキーを消し、モデルを空にして、**探索・推論・保存・有効化を行いません**。トークン入力後、明示的に**モデル読み込み**を押すと推論せずに一覧を取得します。保存と有効化は別操作です。**接続テスト**はクリック時だけモデル要求を実行し、メタデータの取得成功は推論可能という証明ではありません。 -**過去の無料モデル検証 — 2026-10-25 KST:** 一覧にある場合、`opencode/deepseek-v4.1-flash-free` を選べます。当時の未ログイン macOS arm64 実行では本物の CLI 2.1.4 を使い、UI 接続テスト、手動 ASK 承認後のホスト Bash 書き込み、正確な `FREE_OK` と改行のファイル内容、最終回答が成功しました。推論 POST **3 回**、出力上限 **1,024**、再試行無効、有料代替なしで、ネイティブツール実行は観測していません。これは当時の小さな隔離ワークフローだけの証拠であり、今回の再実行、永続提供、無制限利用を示しません。現在の提供状況、資格、制限を確認してください。旧版のホステッド無料アクセスは未検証です。 +**過去の無料モデル検証 — 2026-10-25 KST:** 一覧にある場合、`opencode/deepseek-v4.1-flash-free` を選べます。当時の未ログイン macOS arm64 実行では本物の CLI 2.1.4 を使い、UI 接続テスト、手動 ASK 承認後のホスト Bash 書き込み、正確な `FREE_OK` と改行のファイル内容、最終回答が成功しました。推論 POST **3 回**、再試行無効、有料代替なしで、ネイティブツール実行は観測していません。これは当時の小さな隔離ワークフローだけの証拠であり、今回の再実行、永続提供、無制限利用を示しません。現在の提供状況、資格、制限を確認してください。旧版のホステッド無料アクセスは未検証です。 編集中に空トークンで保存値を保持できるのは同じ SDK URL のみです。URL 変更で入力トークンを消し、宛先・形式変更後の空欄では保存認証情報を流用しません。`localhost`、`127.0.0.1`、`::1` は別スコープです。モデル ID/出力上限は現在のメタデータを使います。推論設定はモデル・プロバイダー次第で、未対応なら明示的に失敗します。 diff --git a/docs/ko/llm-providers.md b/docs/ko/llm-providers.md index 938a30c5..5758ae5a 100644 --- a/docs/ko/llm-providers.md +++ b/docs/ko/llm-providers.md @@ -79,7 +79,7 @@ bun run start 템플릿은 저장하지 않은 클라우드 키를 지우고 모델을 빈칸으로 두며, **모델 조회·추론·저장·활성화 요청을 보내지 않습니다**. 브리지 토큰을 입력하고 **모델 불러오기**를 눌러야 추론 없이 목록을 조회합니다. 준비되면 저장하고 별도로 활성화하세요. **연결 테스트**는 눌렀을 때만 모델을 호출합니다. 메타데이터를 조회할 수 있다고 추론 접근 권한까지 확인되는 것은 아닙니다. -**이전 검증의 무료 모델 시작점 — 2026-10-25 KST:** **모델 불러오기**에 표시되면 `opencode/deepseek-v4.1-flash-free`를 선택할 수 있습니다. macOS arm64에서 로그인하지 않은 상태로 실제 native CLI 2.1.4를 사용해 UI 연결 테스트, 수동 ASK 승인 후 호스트 Bash 쓰기, 정확한 `FREE_OK` 파일 내용과 줄바꿈, 최종 어시스턴트 응답을 확인했습니다. 출력 한도 1,024, 재시도 끄기 설정으로 추론 POST 3회를 사용했으며 유료 대체 모델이나 기본 도구 실행은 관찰하지 않았습니다. 작은 격리 작업 하나의 검증이며, 현재 제공 여부·무료 이용 자격·한도는 별도로 적용됩니다. +**이전 검증의 무료 모델 시작점 — 2026-10-25 KST:** **모델 불러오기**에 표시되면 `opencode/deepseek-v4.1-flash-free`를 선택할 수 있습니다. macOS arm64에서 로그인하지 않은 상태로 실제 native CLI 2.1.4를 사용해 UI 연결 테스트, 수동 ASK 승인 후 호스트 Bash 쓰기, 정확한 `FREE_OK` 파일 내용과 줄바꿈, 최종 어시스턴트 응답을 확인했습니다. 재시도 끄기 설정으로 추론 POST 3회를 사용했으며 유료 대체 모델이나 기본 도구 실행은 관찰하지 않았습니다. 작은 격리 작업 하나의 검증이며, 현재 제공 여부·무료 이용 자격·한도는 별도로 적용됩니다. 수정할 때 토큰을 비우면 같은 SDK 엔드포인트에 저장된 값만 유지합니다. URL을 수정하면 입력한 토큰을 지웁니다. 엔드포인트나 형식을 바꾼 뒤 토큰을 비우면 저장된 자격 증명을 재사용하지 않습니다. `localhost`, `127.0.0.1`, `::1` 같은 호스트 별칭은 서로 다른 토큰 범위입니다. 모델 ID와 출력 한도는 현재 네이티브 모델의 메타데이터를 따릅니다. 추론 설정은 모델·제공자의 지원 여부에 따라 제공자에서 실패할 수 있으며, 오류는 명시적으로 알립니다. diff --git a/docs/llm-providers.md b/docs/llm-providers.md index 07fdc206..65f4cf50 100644 --- a/docs/llm-providers.md +++ b/docs/llm-providers.md @@ -79,7 +79,7 @@ Keep this token available for the profile's **Bridge token** field. The default The template clears an unsaved cloud key, leaves the model blank and makes **no model, inference, save or activation request**. Enter the bridge token and click **Load models** to read its catalog without inference. Save when ready and activate separately. **Test connection** performs a model request only when clicked; metadata discovery does not establish inference access. -**Prior verified free-model starting point — 2026-10-25 KST:** choose `opencode/deepseek-v4.1-flash-free` from **Load models** when it is listed. A signed-out macOS arm64 run through genuine native CLI 2.1.4 passed the UI connection test, a host Bash write after manual ASK approval, exact `FREE_OK` file contents plus newline, and the final assistant answer. It used 3 inference POSTs, an output cap of 1,024 and disabled retries, with no paid fallback or observed native built-in execution. This proves one small isolated workflow; current availability, free-tier eligibility and limits still apply. +**Prior verified free-model starting point — 2026-10-25 KST:** choose `opencode/deepseek-v4.1-flash-free` from **Load models** when it is listed. A signed-out macOS arm64 run through genuine native CLI 2.1.4 passed the UI connection test, a host Bash write after manual ASK approval, exact `FREE_OK` file contents plus newline, and the final assistant answer. It used 3 inference POSTs with retries disabled, with no paid fallback or observed native built-in execution. This proves one small isolated workflow; current availability, free-tier eligibility and limits still apply. When editing, a blank token retains the saved value only for the same SDK endpoint. Changing the URL clears an entered token; changing the endpoint or format with a blank token does not reuse the saved credential. Host aliases such as `localhost`, `127.0.0.1` and `::1` are distinct token scopes. Model IDs and output limits use current native model metadata. Reasoning settings depend on model/provider support and can fail at the provider; errors remain explicit. diff --git a/docs/zh-Hans/llm-providers.md b/docs/zh-Hans/llm-providers.md index a063fa8a..19e135f0 100644 --- a/docs/zh-Hans/llm-providers.md +++ b/docs/zh-Hans/llm-providers.md @@ -83,7 +83,7 @@ bun run start 模板清除未保存的云端密钥,模型留空,**不发现、不推理、不保存、不启用**。填入令牌后明确点击**读取模型**,可不执行推理而查看目录;准备好后保存并单独启用。**测试连接**只在点击时发起模型请求,元数据发现不代表推理访问成功。 -**先前免费模型验证 — 2026-10-25 KST:** 当目录列出时,可从**读取模型**中选择 `opencode/deepseek-v4.1-flash-free`。此前未登录的 macOS arm64 运行通过真正的原生 CLI 2.1.4 完成 UI 连接测试、手动 ASK 批准后的宿主 Bash 写入、精确的 `FREE_OK` 加换行文件内容及最终助手答案。共 **3 次推理 POST**,输出上限 **1,024**,关闭重试,没有付费回退,也未观察到内置工具执行。这只证明当时一个小型隔离流程;不代表本次重跑、永久可用或无限免费。当前模型、免费资格及限额仍需确认。旧版托管免费访问尚未验证。 +**先前免费模型验证 — 2026-10-25 KST:** 当目录列出时,可从**读取模型**中选择 `opencode/deepseek-v4.1-flash-free`。此前未登录的 macOS arm64 运行通过真正的原生 CLI 2.1.4 完成 UI 连接测试、手动 ASK 批准后的宿主 Bash 写入、精确的 `FREE_OK` 加换行文件内容及最终助手答案。共 **3 次推理 POST**,关闭重试,没有付费回退,也未观察到内置工具执行。这只证明当时一个小型隔离流程;不代表本次重跑、永久可用或无限免费。当前模型、免费资格及限额仍需确认。旧版托管免费访问尚未验证。 编辑时,空令牌只在同一 SDK 地址保留已保存值。更改 URL 会清除输入的令牌;更改地址或格式后留空不会复用已保存凭证。`localhost`、`127.0.0.1`、`::1` 是不同凭证作用域。模型 ID 与输出限制以当前元数据为准。思考设置依赖模型/供应商支持,不支持时明确失败。 diff --git a/docs/zh-Hant/llm-providers.md b/docs/zh-Hant/llm-providers.md index 444e5ada..4eb55301 100644 --- a/docs/zh-Hant/llm-providers.md +++ b/docs/zh-Hant/llm-providers.md @@ -83,7 +83,7 @@ bun run start 模板清除未保存的雲端密鑰,模型留空,**不發現、不推理、不保存、不啓用**。填入權杖後明確點擊**讀取模型**,可不執行推理而查看目錄;準備好後保存並單獨啓用。**測試連接**只在點擊時發起模型請求,元資料發現不代表推理訪問成功。 -**先前免費模型驗證 — 2026-10-25 KST:** 當目錄列出時,可從**讀取模型**中選擇 `opencode/deepseek-v4.1-flash-free`。此前未登錄的 macOS arm64 運行通過真正的原生 CLI 2.1.4 完成 UI 連接測試、手動 ASK 批准後的宿主 Bash 寫入、精確的 `FREE_OK` 加換行文件內容及最終助手答案。共 **3 次推理 POST**,輸出上限 **1,024**,關閉重試,沒有付費回退,也未觀察到內建工具執行。這只證明當時一個小型隔離流程;不代表本次重跑、永久可用或無限免費。當前模型、免費資格及限額仍需確認。舊版托管免費訪問尚未驗證。 +**先前免費模型驗證 — 2026-10-25 KST:** 當目錄列出時,可從**讀取模型**中選擇 `opencode/deepseek-v4.1-flash-free`。此前未登錄的 macOS arm64 運行通過真正的原生 CLI 2.1.4 完成 UI 連接測試、手動 ASK 批准後的宿主 Bash 寫入、精確的 `FREE_OK` 加換行文件內容及最終助手答案。共 **3 次推理 POST**,關閉重試,沒有付費回退,也未觀察到內建工具執行。這只證明當時一個小型隔離流程;不代表本次重跑、永久可用或無限免費。當前模型、免費資格及限額仍需確認。舊版托管免費訪問尚未驗證。 編輯時,空權杖只在同一 SDK 地址保留已保存值。更改 URL 會清除輸入的權杖;更改地址或格式後留空不會復用已保存憑證。`localhost`、`127.0.0.1`、`::1` 是不同憑證作用域。模型 ID 與輸出限制以當前元資料為準。思考設置依賴模型/供應商支持,不支持時明確失敗。 diff --git a/server/opencode_sdk_test.go b/server/opencode_sdk_test.go index 66382d07..ea2c6bb1 100644 --- a/server/opencode_sdk_test.go +++ b/server/opencode_sdk_test.go @@ -161,6 +161,97 @@ func TestSDKStoredTokenMatchesEndpointAndFormat(t *testing.T) { } } } + +func TestSDKConnectionHTTPHonorsFormAndStoredTokenLimits(t *testing.T) { + t.Setenv("SCOPEWEAVER_CONFIG", filepath.Join(t.TempDir(), "missing.json")) + t.Setenv("SCOPEWEAVER_LOCAL_LLM_ONLY", "false") + m := backportTestManager(t) + for _, stream := range []bool{false, true} { + for _, tc := range []struct { + name string + form, stored, want int + }{ + {"submitted", 1024, 0, 1024}, + {"stored profile", 0, 1024, 1024}, + {"submitted overrides stored", 512, 2048, 512}, + {"unspecified", 0, 0, 8192}, + } { + t.Run(fmt.Sprintf("%s/stream=%v", tc.name, stream), func(t *testing.T) { + var calls, maxTokens atomic.Int32 + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer bridge-token" { + t.Error("connection request did not use its scoped token") + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + if r.URL.Path == "/v1/capabilities" { + fmt.Fprint(w, sdkCaps) + return + } + if r.Method == http.MethodDelete { + return + } + var body struct { + MaxTokens int `json:"max_tokens"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Error(err) + http.Error(w, "invalid request", http.StatusBadRequest) + return + } + calls.Add(1) + maxTokens.Store(int32(body.MaxTokens)) + if stream { + w.Header().Set("Content-Type", "application/x-ndjson") + fmt.Fprintln(w, `{"type":"message_start"}`) + fmt.Fprintln(w, `{"type":"text_delta","text":"OK"}`) + fmt.Fprintln(w, `{"type":"message_delta","stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}`) + fmt.Fprintln(w, `{"type":"message_stop"}`) + } else { + fmt.Fprint(w, `{"message":{"role":"assistant","content":[{"type":"text","text":"OK"}]},"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}`) + } + })) + defer bridge.Close() + s := &Server{m: m} + body := map[string]any{"provider": opencodesdk.Format, "model": "opencode/test", "base_url": bridge.URL, "streaming": stream} + if tc.form > 0 { + body["max_tokens"] = tc.form + } + if tc.stored > 0 { + id, err := m.pg.SaveProfile(&db.LLMProfile{Name: "SDK-connection-token-cap", Format: opencodesdk.Format, BaseURL: bridge.URL, Model: "opencode/test", APIKey: "bridge-token", MaxTokens: tc.stored, Streaming: stream}) + if err != nil { + t.Fatal(err) + } + defer m.pg.DeleteProfile(id) + body["profile_id"] = id + } else { + body["api_key"] = "bridge-token" + } + api := httptest.NewServer(http.HandlerFunc(s.testLLM)) + defer api.Close() + raw, err := json.Marshal(body) + if err != nil { + t.Fatal(err) + } + response, err := api.Client().Post(api.URL, "application/json", bytes.NewReader(raw)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + var got struct { + OK bool `json:"ok"` + Reply, Error string + } + if err := json.NewDecoder(response.Body).Decode(&got); err != nil { + t.Fatal(err) + } + if response.StatusCode != http.StatusOK || !got.OK || got.Reply != "OK" || calls.Load() != 1 || int(maxTokens.Load()) != tc.want { + t.Fatalf("HTTP %d result=%+v inference calls=%d max_tokens=%d want 1/%d", response.StatusCode, got, calls.Load(), maxTokens.Load(), tc.want) + } + }) + } + } +} func TestSDKMetadataLocalOnlyRejectedBeforeBridge(t *testing.T) { t.Setenv("SCOPEWEAVER_CONFIG", filepath.Join(t.TempDir(), "missing.json")) t.Setenv("SCOPEWEAVER_LOCAL_LLM_ONLY", "true") diff --git a/server/server.go b/server/server.go index 0a6abc84..88329d90 100644 --- a/server/server.go +++ b/server/server.go @@ -1466,6 +1466,9 @@ func (s *Server) testLLM(w http.ResponseWriter, r *http.Request) { // reasoning_effort/thinking field fails the test too (no false "test ok, run 400"). cfg.ThinkingType = req.ThinkingType cfg.ReasoningEffort = req.ReasoningEffort + if cfg.Provider() == opencodesdk.Format && req.MaxTokens > 0 { + cfg.MaxTokens = req.MaxTokens + } // Test the profile's chosen transport mode so endpoints supporting only one mode fail here, // rather than passing the test but failing in an actual conversation. if req.Streaming != nil { From 4144f146606fec1eb465ef82f56b00167f1024f3 Mon Sep 17 00:00:00 2001 From: cskwork Date: Sun, 25 Oct 2026 22:22:41 +0900 Subject: [PATCH 3/6] docs: clarify tested OpenCode runtime platforms --- docs/es/llm-providers.md | 2 +- docs/ja/llm-providers.md | 2 +- docs/ko/llm-providers.md | 2 +- docs/llm-providers.md | 2 +- docs/zh-Hans/llm-providers.md | 2 +- docs/zh-Hant/llm-providers.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/es/llm-providers.md b/docs/es/llm-providers.md index e725d3ba..f79802fc 100644 --- a/docs/es/llm-providers.md +++ b/docs/es/llm-providers.md @@ -49,7 +49,7 @@ Verificado con documentación oficial el **2026-10-03**. La disponibilidad, dere El modo nativo usa el CLI auténtico fijado con `serve --stdio` y `run --server --session`; el cliente oficial gestiona sesiones, eventos e interrupción. Cada llamada controla home/configuración/datos temporales privados y su base de datos. Conserva las definiciones e implementaciones nativas reales, con permisos ASK y sin `--auto`; un hook público `execute.before` bloquea todas las herramientas ajenas al anfitrión antes de ejecutarse. Solo devuelve las capturas a Norma para ejecución y aprobación. Conserva el contexto generado de modelo/entorno/herramientas junto con system e historial del anfitrión. Rechaza fuentes de configuración de sistema/MDM no controladas sin leerlas ni modificarlas; consulta la guía del adaptador. -Los proveedores personalizados heredados usan un mapeo explícito compatible con OpenAI. **Ninguno de los dos entornos admite Windows**, porque no está implementada la limpieza necesaria del árbol POSIX. La verificación real previa cubrió **macOS arm64**. Linux CI está configurada, pero su ejecución alojada sigue sin verificarse. No se afirman resultados en otras plataformas; versiones SDK no probadas requieren verificación independiente. +Los proveedores personalizados heredados usan un mapeo explícito compatible con OpenAI. **Ninguno de los dos entornos admite Windows**, porque no está implementada la limpieza necesaria del árbol POSIX. La verificación local del entorno cubre **macOS arm64**. CI ejecuta pruebas con el entorno real en **Linux x64**; no se afirma que otras plataformas estén verificadas. Las versiones SDK no probadas requieren verificación independiente. Sigue el [inicio del adaptador](../../adapters/opencode-sdk/README.es.md) con **Bun 1.3.14**. El puente tiene configuración propia y no importa credenciales guardadas de OpenCode ni de nube. Define un `SCOPEWEAVER_OPENCODE_TOKEN` independiente de al menos **32 caracteres no blancos** y úsalo en **LLM → Nuevo → Plantilla de configuración → Puente OpenCode SDK**. No pegues una clave API de OpenCode Go/Console. diff --git a/docs/ja/llm-providers.md b/docs/ja/llm-providers.md index 6b2745e4..98bd5cc7 100644 --- a/docs/ja/llm-providers.md +++ b/docs/ja/llm-providers.md @@ -54,7 +54,7 @@ GLM-5.3 はテキストを受け付け、コンテキストは 1M トークン ネイティブは固定した本物の CLI `serve --stdio`、`run --server --session` と公式クライアントでセッション、イベント、中断を処理します。呼び出しごとに私有の一時 home/設定/データと DB を所有します。実際の組み込みツール定義・実装は残しますが、権限は ASK、`--auto` は使わず、公開 `execute.before` が非ホストツールをすべて実行前に拒否します。ホスト収集要求だけを Norma に戻して実行・承認します。OpenCode のモデル/環境/ツール文脈をホスト system と履歴に加えて保持します。管理できないシステム/MDM 設定がある場合の起動拒否は、読み取り・変更を行わずに適用します。詳細はアダプターガイドにあります。 -旧版のカスタムプロバイダーは明示的な OpenAI 互換マッピングを使います。必要な POSIX プロセスツリー終了処理が未実装のため、**両ランタイムとも Windows 非対応**です。過去の実ランタイムテスト範囲は **macOS arm64**。Linux CI は設定済みですが、ホステッド結果は未検証です。他のプラットフォームや未テスト SDK バージョンの動作は保証しません。 +旧版のカスタムプロバイダーは明示的な OpenAI 互換マッピングを使います。必要な POSIX プロセスツリー終了処理が未実装のため、**両ランタイムとも Windows 非対応**です。ローカルのランタイム検証範囲は **macOS arm64** です。CI は **Linux x64** で実際のランタイムを使うテストを実行します。他のプラットフォームを検証済みとはしません。未テストの SDK バージョンには別途検証が必要です。 [アダプターの起動手順](../../adapters/opencode-sdk/README.ja.md)に従い **Bun 1.3.14** を使います。ブリッジは独自設定を持ち、保存済み OpenCode/クラウド認証情報を取り込みません。空白以外の文字 **32 文字以上**の独立した `SCOPEWEAVER_OPENCODE_TOKEN` を設定し、**LLM → 新規 → 設定テンプレート → OpenCode SDK ブリッジ**に入力します。OpenCode Go/Console API キーを貼り付けないでください。 diff --git a/docs/ko/llm-providers.md b/docs/ko/llm-providers.md index 5758ae5a..1fcb7b20 100644 --- a/docs/ko/llm-providers.md +++ b/docs/ko/llm-providers.md @@ -49,7 +49,7 @@ GLM-5.3은 텍스트 입력과 100만 토큰의 문맥 창을 지원하며 추 `native`는 고정한 실제 CLI의 `serve --stdio`와 `run --server --session`을 실행하며, 공식 클라이언트가 모델 조회·이벤트·중단을 관찰하고 제어합니다. 호출마다 비공개 임시 CLI home/config/data 경로와 데이터베이스 저장소를 소유합니다. 실제 기본 도구 정의와 원래 구현은 등록된 상태로 유지합니다. 기본 도구 권한은 ASK이며, 별도의 공개 `execute.before` 가드가 모든 비호스트 도구의 실행을 사전에 거부합니다. 수집한 호스트 도구 요청은 Norma가 실행·승인하도록 반환합니다. 호스트의 시스템 지침과 기록에 OpenCode가 생성한 모델·환경·도구 문맥도 유지됩니다. 관리되지 않는 시스템·MDM 설정이 있으면 실행하지 않는 검사는 어댑터 안내를 참고하세요. 해당 설정을 읽거나 변경하지 않습니다. -`legacy`의 사용자 지정 제공자는 어댑터 안내의 명시적인 OpenAI 호환 매핑을 사용합니다. 필요한 POSIX 프로세스 트리 정리 경계가 구현되지 않아 **두 런타임 모두 Windows에서 지원하지 않습니다.** 실제 런타임 테스트는 **macOS arm64**에서 실행했습니다. Linux CI는 구성했지만 아직 호스팅된 실행으로 Linux 런타임 결과를 확인하지 않았습니다. 그 밖의 플랫폼도 검증했다고 주장하지 않습니다. 테스트하지 않은 SDK 버전은 별도 검증이 필요합니다. +`legacy`의 사용자 지정 제공자는 어댑터 안내의 명시적인 OpenAI 호환 매핑을 사용합니다. 필요한 POSIX 프로세스 트리 정리 경계가 구현되지 않아 **두 런타임 모두 Windows에서 지원하지 않습니다.** 로컬 런타임 검증 범위는 **macOS arm64**입니다. CI는 **Linux x64**에서 실제 런타임 테스트를 실행하며, 그 밖의 플랫폼을 검증했다고 주장하지 않습니다. 테스트하지 않은 SDK 버전은 별도 검증이 필요합니다. [어댑터 설정 안내](../../adapters/opencode-sdk/README.ko.md)에 따라 Bun 브리지를 실행하세요. 브리지는 자체 제공자 설정을 사용하며, 사용자가 저장한 OpenCode 또는 클라우드 제공자 자격 증명을 가져오지 않습니다. 브리지에 별도의 `SCOPEWEAVER_OPENCODE_TOKEN`을 설정한 뒤(공백 없는 32자 이상), **LLM → 새로 만들기 → 설정 템플릿 → OpenCode SDK · 브리지**를 선택하세요. diff --git a/docs/llm-providers.md b/docs/llm-providers.md index 65f4cf50..20d50965 100644 --- a/docs/llm-providers.md +++ b/docs/llm-providers.md @@ -49,7 +49,7 @@ This separate backend supports two pinned runtimes through the same `opencode-sd `native` starts the genuine pinned CLI through `serve --stdio` and `run --server --session`; the official client handles discovery, event observation and interruption. Each call owns private temporary CLI home/config/data paths and database storage. Genuine native tool definitions and original implementations remain registered: native permissions are ASK, and a separate public `execute.before` guard denies every non-host tool before execution. Host capture requests return to Norma for execution and approvals. OpenCode's generated model/environment/tool context remains alongside the host's system instructions and history. See the adapter guide for the fail-closed unmanaged system/MDM configuration checks; those sources are not read or changed. -Custom providers for `legacy` use the explicit OpenAI-compatible mapping in the adapter guide. **Both runtimes are unsupported on Windows** because the required POSIX process-tree cleanup boundary is not implemented there. Actual runtime tests were run on **macOS arm64**. Linux CI is configured, but its hosted run has not yet established Linux runtime results; other platforms are not claimed as verified. Untested SDK versions require separate verification. +Custom providers for `legacy` use the explicit OpenAI-compatible mapping in the adapter guide. **Both runtimes are unsupported on Windows** because the required POSIX process-tree cleanup boundary is not implemented there. Local runtime verification covers **macOS arm64**. CI runs actual-runtime fixtures on **Linux x64**; other platforms are not claimed verified. Untested SDK versions require separate verification. Start the Bun bridge using the [adapter setup instructions](../adapters/opencode-sdk/README.md). It has its own provider configuration; it does not import saved user OpenCode or cloud-provider credentials. Set a separate `SCOPEWEAVER_OPENCODE_TOKEN` on the bridge (at least 32 non-whitespace characters), then open **LLM → New → Configuration template → OpenCode SDK · bridge**. diff --git a/docs/zh-Hans/llm-providers.md b/docs/zh-Hans/llm-providers.md index 19e135f0..16a584f1 100644 --- a/docs/zh-Hans/llm-providers.md +++ b/docs/zh-Hans/llm-providers.md @@ -55,7 +55,7 @@ GLM-5.3 接受文字,上下文为 1M token,且必须保持推理启用。支 原生模式通过实际固定 CLI 的 `serve --stdio`、`run --server --session` 及官方客户端管理会话、事件和中断。每次调用拥有私有临时 home/配置/数据路径和数据库。保留真实内置工具定义及原始实现;原生权限为 ASK,不使用 `--auto`,公开 `execute.before` 钩子在执行前阻止所有非宿主工具。只有收集的宿主请求交回 Norma 执行和审批。OpenCode 生成的模型/环境/工具上下文与宿主 system、历史共同保留。遇到无法受控的系统/MDM 配置来源时拒绝启动,不读取或修改它们;细节见适配器指南。 -旧版自定义供应商使用显式 OpenAI 兼容配置。**两个运行时均不支持 Windows**,因为必需的 POSIX 进程树清理边界尚未实现。先前实际运行测试范围是 **macOS arm64**;Linux CI 已配置,但托管结果尚待验证。其他平台不声称已验证;其他 SDK 版本需单独验证。 +旧版自定义供应商使用显式 OpenAI 兼容配置。**两个运行时均不支持 Windows**,因为必需的 POSIX 进程树清理边界尚未实现。本地运行时验证覆盖 **macOS arm64**。CI 在 **Linux x64** 上运行实际运行时测试;不声称已验证其他平台。其他 SDK 版本需单独验证。 按[适配器启动指南](../../adapters/opencode-sdk/README.zh-Hans.md)使用 **Bun 1.3.14**。桥接器使用自己的供应商配置,不导入用户 OpenCode 或云端已保存凭证。设置至少 **32 个非空白字符**的独立 `SCOPEWEAVER_OPENCODE_TOKEN`,在 **LLM → 新增 → 配置模板 → OpenCode SDK 桥接器**中填入;不要粘贴 OpenCode Go/Console API 密钥。 diff --git a/docs/zh-Hant/llm-providers.md b/docs/zh-Hant/llm-providers.md index 4eb55301..16b9b087 100644 --- a/docs/zh-Hant/llm-providers.md +++ b/docs/zh-Hant/llm-providers.md @@ -55,7 +55,7 @@ GLM-5.3 接受文字,上下文為 1M token,且必須保持推理啟用。支 原生模式通過實際固定 CLI 的 `serve --stdio`、`run --server --session` 及官方客戶端管理工作階段、事件和中斷。每次調用擁有私有臨時 home/設定/資料路徑和資料庫。保留真實內建工具定義及原始實現;原生權限為 ASK,不使用 `--auto`,公開 `execute.before` 鈎子在執行前阻止所有非宿主工具。只有收集的宿主請求交回 Norma 執行和審批。OpenCode 生成的模型/環境/工具上下文與宿主 system、歷史共同保留。遇到無法受控的系統/MDM 設定來源時拒絕啓動,不讀取或修改它們;細節見適配器指南。 -舊版自定義供應商使用顯式 OpenAI 兼容設定。**兩個運行時均不支持 Windows**,因為必需的 POSIX 進程樹清理邊界尚未實現。先前實際運行測試範圍是 **macOS arm64**;Linux CI 已設定,但托管結果尚待驗證。其他平台不聲稱已驗證;其他 SDK 版本需單獨驗證。 +舊版自定義供應商使用顯式 OpenAI 兼容設定。**兩個運行時均不支持 Windows**,因為必需的 POSIX 進程樹清理邊界尚未實現。本機執行環境驗證涵蓋 **macOS arm64**。CI 在 **Linux x64** 執行實際執行環境測試;不宣稱已驗證其他平台。其他 SDK 版本需單獨驗證。 按[適配器啓動指南](../../adapters/opencode-sdk/README.zh-Hant.md)使用 **Bun 1.3.14**。橋接器使用自己的供應商設定,不導入用戶 OpenCode 或雲端已保存憑證。設置至少 **32 個非空白字元**的獨立 `SCOPEWEAVER_OPENCODE_TOKEN`,在 **LLM → 新增 → 設定模板 → OpenCode SDK 橋接器**中填入;不要粘貼 OpenCode Go/Console API 密鑰。 From b9781b2c4b92d9cef4a048f1c897616ce9ed8ff9 Mon Sep 17 00:00:00 2001 From: cskwork Date: Mon, 26 Oct 2026 01:41:53 +0900 Subject: [PATCH 4/6] test(e2e): capture provider attribution diagnostics --- e2e/chat-context.mjs | 12 ++-- e2e/chat-controls-runner.mjs | 22 ++++++- server/chat_controls_e2e_test.go | 109 ++++++++++++++++++++++++++++++- 3 files changed, 133 insertions(+), 10 deletions(-) diff --git a/e2e/chat-context.mjs b/e2e/chat-context.mjs index e8b489d6..da332b76 100644 --- a/e2e/chat-context.mjs +++ b/e2e/chat-context.mjs @@ -8,7 +8,7 @@ const labels = namespaceMessages("interface"); async function closeContext(page, label) { const button=page.getByRole("button",{name:label,exact:true}); try { await button.click(); } catch(error) { - console.error("context-close-diagnostic",await page.evaluate(()=>({animations:document.getAnimations().map(a=>({state:a.playState,time:a.currentTime,target:a.effect?.target?.outerHTML?.slice(0,500)})),drawer:document.querySelector("[data-vaul-drawer]")?.outerHTML.slice(0,1800),panel:document.querySelector("[data-chat-context-panel]")?.getBoundingClientRect().toJSON()}))); + try { console.error("context-close-diagnostic",await page.evaluate(()=>({animations:document.getAnimations().map(a=>({state:a.playState,time:a.currentTime,target:a.effect?.target?.outerHTML?.slice(0,500)})),drawer:document.querySelector("[data-vaul-drawer]")?.outerHTML.slice(0,1800),panel:document.querySelector("[data-chat-context-panel]")?.getBoundingClientRect().toJSON()}))); } catch(captureError) { console.error("context-close-capture-failure",String(captureError)); } throw error; } } @@ -26,14 +26,14 @@ export async function runChatContextChecks({base,token,fixture,step,check,openPa const p=await openPage(locale,token);await p.setViewportSize({width,height:900}); try{await step(`[${locale}/${width}/${surface}] context visibility, capacity switch and manual compaction`,async()=>{ await p.goto(url);const input=p.locator("textarea[data-chat-draft]").first();await input.waitFor();await p.locator("[data-chat-context-trigger]").waitFor(); - const calls=await fixture.calls();await input.fill("/context");await p.getByRole("button",{name:t.m0073,exact:true}).click();await p.locator("[data-chat-context-panel]").waitFor();await p.getByText(t.chatContextUnknown,{exact:true}).last().waitFor();check(await p.locator("[data-chat-compact]").isDisabled(),"unobserved context allows compact");check(await fixture.calls()===calls,"/context called the provider");await closeContext(p,t.chatContextClose); + const calls=await fixture.calls({operation:"context-command",boundary:"before"});await input.fill("/context");await p.getByRole("button",{name:t.m0073,exact:true}).click();await p.locator("[data-chat-context-panel]").waitFor();await p.getByText(t.chatContextUnknown,{exact:true}).last().waitFor();check(await p.locator("[data-chat-compact]").isDisabled(),"unobserved context allows compact");check(await fixture.calls({operation:"context-command",boundary:"after"})===calls,"/context called the provider");await closeContext(p,t.chatContextClose); for(let i=0;i<5;i++){await input.fill(`context turn ${i}: `+(i===0?`${caseMarker} `:"")+"preserve detail ".repeat(120));if(i===0)await fixture.holdNext();await p.getByRole("button",{name:t.m0073,exact:true}).click(); if(i===0){await waitChatState(api,path,state=>!!state.active);await p.locator("[data-chat-context-trigger]").click();await p.locator("[data-chat-context-panel]").waitFor();if(process.env.CHAT_CONTEXT_MUTATION)await p.locator("[data-chat-compact]").evaluate(button=>button.disabled=false);check(await p.locator("[data-chat-compact]").isDisabled(),"busy Compact now is enabled");await closeContext(p,t.chatContextClose);await fixture.release()} await waitChatState(api,path,state=>!state.active&&state.latest_run_status==="completed");await p.getByRole("button",{name:t.chatEdit,exact:true}).nth(i).waitFor(); } await p.locator("[data-chat-context-trigger]").click();await p.locator("[data-chat-context-panel]").waitFor();await p.getByText("fixture-64",{exact:true}).waitFor();await p.waitForFunction(()=>{const panel=document.querySelector("[data-chat-context-panel]");if(!panel)return false;const box=panel.getBoundingClientRect();return box.y>=0&&box.x>=0&&box.right<=innerWidth+1});const box=await p.locator("[data-chat-context-panel]").boundingBox();check(box&&box.y>=0&&box.x>=0&&box.x+box.width<=width+1,`context panel extends outside the viewport: ${JSON.stringify(box)}`);check(await p.getByLabel(t.chatContextKeep,{exact:true}).inputValue()==="2","default retention changed"); - const before=await (await api.get(actionURL(path,"context"))).json();check(before.window===64000&&before.measured_input===80,"provider input anchor missing");const count=await fixture.calls();await fixture.setBudget(32); - const switched=await (await api.get(actionURL(path,"context"))).json();check(switched.window===32000&&switched.measured_input===0&&switched.estimated,"stale anchor survived model switch");check(await fixture.calls()===count,"capacity inspection called provider");await p.getByText("fixture-32",{exact:true}).waitFor(); + const before=await (await api.get(actionURL(path,"context"))).json();check(before.window===64000&&before.measured_input===80,"provider input anchor missing");const count=await fixture.calls({operation:"capacity-switch",boundary:"before"});await fixture.setBudget(32); + const switched=await (await api.get(actionURL(path,"context"))).json();check(switched.window===32000&&switched.measured_input===0&&switched.estimated,"stale anchor survived model switch");check(await fixture.calls({operation:"capacity-switch",boundary:"after"})===count,"capacity inspection called provider");await p.getByText("fixture-32",{exact:true}).waitFor(); const state=await (await api.get(path)).json();const expectedFocus="Preserve URLs";await p.getByLabel(t.chatContextFocus,{exact:true}).fill(expectedFocus); const stripFocus=process.env.CHAT_CONTEXT_STRIP_FOCUS_CASE===caseName; const compactURL=withLocaleQuery(`${base}${actionURL(path,"compact")}`,locale);let focusMutation; @@ -46,10 +46,10 @@ export async function runChatContextChecks({base,token,fixture,step,check,openPa await route.continue({postData:JSON.stringify(mutated)}); }; if(stripFocus)await p.route(compactURL,stripCurrentFocus); - const requestCursor=(await fixture.requests()).length; + const requestCursor=(await fixture.requests({operation:"manual-compact",boundary:"before",marker:caseMarker})).length; await p.locator("[data-chat-compact]").click();await p.getByText(t.chatContextSuccess,{exact:true}).waitFor(); if(stripFocus){await p.unroute(compactURL,stripCurrentFocus);check(!!focusMutation,"designated compact POST was not focus-mutated")} - const compacted=await (await api.get(actionURL(path,"context"))).json();check(Number(compacted.history_revision)===Number(state.history_revision)+1,"compact did not publish revision");check(compacted.summary&&compacted.usedcategory.estimated),"local categories presented as measured");check(compacted.categories.some(category=>category.name==="system")&&compacted.categories.some(category=>category.name==="conversation"),"manual compaction lost category attribution");check(compacted.before_tokens===switched.categories.reduce((sum,category)=>sum+category.tokens,0),"before/after mixes measured usage with estimates");const captured=await fixture.requests(); + const compacted=await (await api.get(actionURL(path,"context"))).json();check(Number(compacted.history_revision)===Number(state.history_revision)+1,"compact did not publish revision");check(compacted.summary&&compacted.usedcategory.estimated),"local categories presented as measured");check(compacted.categories.some(category=>category.name==="system")&&compacted.categories.some(category=>category.name==="conversation"),"manual compaction lost category attribution");check(compacted.before_tokens===switched.categories.reduce((sum,category)=>sum+category.tokens,0),"before/after mixes measured usage with estimates");const captured=await fixture.requests({operation:"manual-compact",boundary:"after",marker:caseMarker}); console.log("context-summary-capture",JSON.stringify({case:caseName,marker:caseMarker,cursor:requestCursor,requestCount:captured.length,focusMutated:!!focusMutation,requests:captured.slice(requestCursor)})); const attempts=assertCurrentSummaryFocus(captured,{cursor:requestCursor,marker:caseMarker,focus:expectedFocus}); console.log("context-summary-attempts",JSON.stringify({case:caseName,marker:caseMarker,cursor:requestCursor,indices:attempts.map(attempt=>attempt.index)})); diff --git a/e2e/chat-controls-runner.mjs b/e2e/chat-controls-runner.mjs index 13828eaf..3df956b2 100644 --- a/e2e/chat-controls-runner.mjs +++ b/e2e/chat-controls-runner.mjs @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import {writeFile} from "node:fs/promises"; import {chromium,request} from "playwright"; import {runChatQueueChecks,runChatHistoryChecks,waitChatState} from "./chat-controls.mjs"; import {runChatPasteChecks} from "./chat-paste.mjs"; @@ -10,11 +11,28 @@ const api=await request.newContext({baseURL:base}); const init=await api.post("/api/auth/init",{data:{password:"chat-controls-pass-1234"}});assert.ok(init.ok(),await init.text()); const login=await api.post("/api/auth/login",{data:{username:"ARTEX",password:"chat-controls-pass-1234"}});assert.ok(login.ok(),await login.text());const {token}=await login.json(); const fixtureRequest=async(action,body)=>{const response=await api.post(`/__fixture/${action}`,{headers:{"X-Chat-Fixture":process.env.SCOPEWEAVER_E2E_FIXTURE},data:body??{}});assert.ok(response.ok(),await response.text());return response.json()}; -const fixture={resetTools:()=>fixtureRequest("reset-tools"),holdNext:()=>fixtureRequest("hold"),release:()=>fixtureRequest("release"),calls:async()=>(await fixtureRequest("stats")).calls,requests:async()=>(await fixtureRequest("stats")).requests,stats:()=>fixtureRequest("stats"),setBudget:window=>fixtureRequest("budget",{window}),seedPendingApproval:async(ref,run,turn)=>(await fixtureRequest("approval",{ref,run,turn})).id,clearPendingApproval:id=>fixtureRequest("clear-approval",{id})}; +let activeCase="setup",readSequence=0,countSequence=0; +const fixtureStats=(kind,metadata={})=>{ + let operation=kind,boundary="observation"; + if(activeCase.includes("queue edits, order")){operation="queue-resume";boundary=kind==="calls"?"before":"after"} + else if(activeCase.includes("retry, edit, branches")&&kind==="calls"){operation="explicit-branch";boundary=++countSequence%2?"before":"after"} + else if(activeCase.includes("running tool steering")&&kind==="calls"){operation="steer-follow-up";boundary=++countSequence%2?"before":"after"} + return fixtureRequest("stats",{diagnostic:{case:activeCase,operation,boundary,read:++readSequence,...metadata}}); +}; +const fixture={resetTools:()=>fixtureRequest("reset-tools"),holdNext:()=>fixtureRequest("hold"),release:()=>fixtureRequest("release"),calls:async metadata=>(await fixtureStats("calls",metadata)).calls,requests:async metadata=>(await fixtureStats("requests",metadata)).requests,stats:metadata=>fixtureStats("stats",metadata),setBudget:window=>fixtureRequest("budget",{window}),seedPendingApproval:async(ref,run,turn)=>(await fixtureRequest("approval",{ref,run,turn})).id,clearPendingApproval:id=>fixtureRequest("clear-approval",{id})}; const browser=await chromium.launch();const errors=[];const pages=[];let failed=0; const started=performance.now(); const elapsed=()=>Math.round(performance.now()-started); -const helpers={base,token,fixture,check:assert.ok,step:async(name,fn)=>{const began=performance.now();console.log(`START ${name} (elapsed ${elapsed()}ms)`);try{await fn();console.log(`PASS ${name} (${Math.round(performance.now()-began)}ms; elapsed ${elapsed()}ms)`)}catch(error){failed++;console.error(`FAIL ${name} (${Math.round(performance.now()-began)}ms; elapsed ${elapsed()}ms): ${error.stack}`);if(process.env.CHAT_QUEUE_RED||process.env.CHAT_CONTEXT_MUTATION)throw error}},openPage:async(locale,jwt)=>{const context=await browser.newContext();context.setDefaultTimeout(15000);await context.addInitScript(([locale,jwt])=>{localStorage.setItem("scopeweaver.locale",locale);localStorage.setItem("artex_token",jwt)},[locale,jwt]);const p=await context.newPage();p.on("pageerror",error=>errors.push(error.message));pages.push(p);return p}}; +const helpers={base,token,fixture,check:assert.ok,step:async(name,fn)=>{activeCase=name;readSequence=0;countSequence=0;const began=performance.now();console.log(`START ${name} (elapsed ${elapsed()}ms)`);try{await fn();console.log(`PASS ${name} (${Math.round(performance.now()-began)}ms; elapsed ${elapsed()}ms)`)}catch(error){failed++;console.error(`FAIL ${name} (${Math.round(performance.now()-began)}ms; elapsed ${elapsed()}ms): ${error.stack}`);// Capture while the failing step still owns its live page, before scenario finally closes it. +for(const [index,p] of pages.entries()){ + if(p.isClosed())continue; + const prefix=`${process.env.SCOPEWEAVER_E2E_ARTIFACTS}/failure-${process.env.CHAT_E2E_SUITE||"standard"}-${failed}-${index}`; + if(!process.env.SCOPEWEAVER_E2E_ARTIFACTS)continue; + for(const [kind,capture] of [["screenshot",()=>p.screenshot({path:`${prefix}.png`,timeout:5000})],["dom",async()=>writeFile(`${prefix}.html`,await p.content(),{mode:0o600})],["state",async()=>writeFile(`${prefix}.json`,JSON.stringify({case:name,error:String(error.stack||error),url:p.url(),page_errors:errors}),{mode:0o600})]]){ + try{await capture()}catch(captureError){console.error("diagnostic-capture-failure",JSON.stringify({case:name,page:index,kind,error:String(captureError.stack||captureError)}))} + } +} +if(process.env.CHAT_QUEUE_RED||process.env.CHAT_CONTEXT_MUTATION)throw error}},openPage:async(locale,jwt)=>{const context=await browser.newContext();context.setDefaultTimeout(15000);await context.addInitScript(([locale,jwt])=>{localStorage.setItem("scopeweaver.locale",locale);localStorage.setItem("artex_token",jwt)},[locale,jwt]);const p=await context.newPage();p.on("pageerror",error=>errors.push(error.message));pages.push(p);return p}}; export async function runChatControlsChecks({base,token,restart,fixture,step,check,openPage}){ const options={base,token,fixture,step,check,openPage};const only=process.env.CHAT_E2E_SUITE;const standard=!only||only==="standard"; if(!only||only==="draft")await runChatDraftRetentionChecks(options); diff --git a/server/chat_controls_e2e_test.go b/server/chat_controls_e2e_test.go index 9db8308a..c2bcbc3e 100644 --- a/server/chat_controls_e2e_test.go +++ b/server/chat_controls_e2e_test.go @@ -1,9 +1,11 @@ package server import ( + "bytes" "context" "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "os" @@ -35,6 +37,9 @@ type chatBrowserFixture struct { toolHold chan struct{} toolStarted bool markerCalls int + providers []map[string]any + snapshots []map[string]any + admissions []map[string]any } func (f *chatBrowserFixture) release() { @@ -50,6 +55,16 @@ func (f *chatBrowserFixture) release() { } } func (f *chatBrowserFixture) model(w http.ResponseWriter, r *http.Request) { + f.mu.Lock() + provider := map[string]any{"ordinal": len(f.providers) + 1, "session": r.Header.Get("X-Chat-E2E-Session"), "arrival": time.Now().UTC().Format(time.RFC3339Nano)} + f.providers = append(f.providers, provider) + f.mu.Unlock() + defer func() { + f.mu.Lock() + defer f.mu.Unlock() + provider["completion"] = time.Now().UTC().Format(time.RFC3339Nano) + provider["context_error"] = fmt.Sprint(r.Context().Err()) + }() var in struct { Stream bool `json:"stream"` Messages []map[string]any `json:"messages"` @@ -65,6 +80,10 @@ func (f *chatBrowserFixture) model(w http.ResponseWriter, r *http.Request) { f.calls++ n := f.calls f.requests = append(f.requests, string(raw)) + provider["count"] = n + provider["count_assignment"] = time.Now().UTC().Format(time.RFC3339Nano) + provider["messages"] = json.RawMessage(raw) + provider["stream"] = in.Stream held := f.hold if main && f.holdArmed { f.holdArmed = false @@ -165,6 +184,36 @@ func TestChatControlsE2E(t *testing.T) { t.Fatal(err) } fixture := &chatBrowserFixture{} + // Test cleanup runs after the existing deferred app/model/listener teardown. + // A missing final file (timeout, cancellation, write failure) is incomplete evidence. + t.Cleanup(func() { + dir, pathErr := filepath.Abs(filepath.Join("..", "dist", "chat-e2e")) + if pathErr != nil { + t.Errorf("diagnostic path: %v", pathErr) + return + } + suite := "standard" + if os.Getenv("CHAT_E2E_SUITE") == "draft" { + suite = "draft" + } + fixture.mu.Lock() + data, marshalErr := json.Marshal(map[string]any{"schema": 1, "suite": suite, "server_time": time.Now().UTC().Format(time.RFC3339Nano), "calls": fixture.calls, "provider_ordinal": len(fixture.providers), "providers": fixture.providers, "snapshots": fixture.snapshots, "admissions": fixture.admissions}) + fixture.mu.Unlock() + if marshalErr != nil { + t.Errorf("diagnostic marshal: %v", marshalErr) + return + } + if dirErr := os.MkdirAll(dir, 0700); dirErr != nil { + t.Errorf("diagnostic directory: %v", dirErr) + return + } + file := filepath.Join(dir, "fixture-ledger-"+suite+".json") + if writeErr := os.WriteFile(file, data, 0600); writeErr != nil { + t.Errorf("diagnostic write %s: %v", file, writeErr) + return + } + t.Logf("diagnostic final ledger: %s (%d bytes)", file, len(data)) + }) model := httptest.NewServer(http.HandlerFunc(fixture.model)) defer model.Close() defer fixture.release() @@ -257,9 +306,15 @@ func TestChatControlsE2E(t *testing.T) { fixture.markerCalls = 0 fixture.mu.Unlock() case "stats": + var in struct { + Diagnostic map[string]any `json:"diagnostic"` + } + _ = json.NewDecoder(r.Body).Decode(&in) fixture.mu.Lock() defer fixture.mu.Unlock() - writeJSON(w, 200, map[string]any{"calls": fixture.calls, "requests": append([]string{}, fixture.requests...), "tool_started": fixture.toolStarted, "marker_calls": fixture.markerCalls}) + snapshot := map[string]any{"case": in.Diagnostic["case"], "operation": in.Diagnostic["operation"], "boundary": in.Diagnostic["boundary"], "read": in.Diagnostic["read"], "marker": in.Diagnostic["marker"], "calls": fixture.calls, "provider_ordinal": len(fixture.providers), "server_time": time.Now().UTC().Format(time.RFC3339Nano)} + fixture.snapshots = append(fixture.snapshots, snapshot) + writeJSON(w, 200, map[string]any{"calls": fixture.calls, "requests": append([]string{}, fixture.requests...), "tool_started": fixture.toolStarted, "marker_calls": fixture.markerCalls, "diagnostic": snapshot}) return case "budget": var in struct { @@ -381,7 +436,31 @@ func TestChatControlsE2E(t *testing.T) { files := http.FileServer(http.Dir("../web/out")) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/api/") { - getApp().Handler().ServeHTTP(w, r) + // Observe existing control traffic without extra application/API requests. + relevant := strings.Contains(r.URL.Path, "/controls") || r.URL.Path == "/api/chat" || (r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/api/conversations/") && strings.HasSuffix(r.URL.Path, "/messages")) || (r.Method != "GET" && (r.URL.Path == "/api/tasks" || r.URL.Path == "/api/conversations" || strings.HasSuffix(r.URL.Path, "/control") || r.URL.Path == "/api/exploration/main-session/new")) + if !relevant { + getApp().Handler().ServeHTTP(w, r) + return + } + var body bytes.Buffer + if r.Body != nil { + r.Body = &chatDiagnosticBody{Reader: io.TeeReader(r.Body, &body), Closer: r.Body} + } + observed := &chatDiagnosticResponse{ResponseWriter: w} + fixture.mu.Lock() + admission := map[string]any{"ordinal": len(fixture.admissions) + 1, "arrival": time.Now().UTC().Format(time.RFC3339Nano), "method": r.Method, "path": r.URL.Path, "query": r.URL.RawQuery} + fixture.admissions = append(fixture.admissions, admission) + fixture.mu.Unlock() + defer func() { + fixture.mu.Lock() + defer fixture.mu.Unlock() + admission["completion"] = time.Now().UTC().Format(time.RFC3339Nano) + admission["status"] = observed.status + admission["request_body_read"] = body.String() + admission["response_body"] = observed.body.String() + admission["context_error"] = fmt.Sprint(r.Context().Err()) + }() + getApp().Handler().ServeHTTP(observed, r) } else { files.ServeHTTP(w, r) } @@ -414,3 +493,29 @@ func nullableFixtureID(id int64) any { } return id } + +// Only the finite JSON control routes above use this writer; SSE routes bypass it. +type chatDiagnosticBody struct { + io.Reader + io.Closer +} +type chatDiagnosticResponse struct { + http.ResponseWriter + status int + body bytes.Buffer +} + +func (w *chatDiagnosticResponse) WriteHeader(status int) { + if w.status == 0 { + w.status = status + } + w.ResponseWriter.WriteHeader(status) +} +func (w *chatDiagnosticResponse) Write(b []byte) (int, error) { + if w.status == 0 { + w.status = http.StatusOK + } + n, err := w.ResponseWriter.Write(b) + w.body.Write(b[:n]) + return n, err +} From 11def121f6e4e49b47147058e36856890cd3f477 Mon Sep 17 00:00:00 2001 From: cskwork <76669236+cskwork@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:24:53 +0900 Subject: [PATCH 5/6] fix(sdk): patch provider utilities and deduplicate Effect --- adapters/opencode-sdk/bun.lock | 58 ++++-------------------------- adapters/opencode-sdk/package.json | 4 +++ 2 files changed, 11 insertions(+), 51 deletions(-) diff --git a/adapters/opencode-sdk/bun.lock b/adapters/opencode-sdk/bun.lock index fa39b6f6..7ee4a9b8 100644 --- a/adapters/opencode-sdk/bun.lock +++ b/adapters/opencode-sdk/bun.lock @@ -20,6 +20,10 @@ }, }, }, + "overrides": { + "@ai-sdk/provider-utils": "4.0.33", + "effect": "4.0.1", + }, "packages": { "@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqcCq2PiFY1dbK/0Ck45KuvE8jfdxRuuAE9Y5w46dAk6U+9vPOeg1CDcmR+ncqmrYrhRl3nmyDttyDahyjCzAw=="], @@ -29,7 +33,7 @@ "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="], "@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="], @@ -143,18 +147,6 @@ "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.3.1", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g=="], - "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], - - "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], - - "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], - - "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - "@npmcli/agent": ["@npmcli/agent@4.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg=="], "@npmcli/arborist": ["@npmcli/arborist@9.4.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/map-workspaces": "^5.0.0", "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^4.0.0", "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^5.0.0", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", "minimatch": "^10.0.3", "nopt": "^9.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.0", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "pacote": "^21.0.2", "parse-conflict-json": "^5.0.1", "proc-log": "^6.0.0", "proggy": "^4.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", "walk-up-path": "^4.0.0" }, "bin": { "arborist": "bin/index.js" } }, "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA=="], @@ -521,8 +513,6 @@ "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], - "fast-check": ["fast-check@4.10.2", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], @@ -531,8 +521,6 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], @@ -581,7 +569,7 @@ "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], - "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], + "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "ip-address": ["ip-address@10.7.5", "", {}, "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA=="], @@ -637,8 +625,6 @@ "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], - "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], @@ -669,12 +655,6 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@2.1.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-JHAdwubWNEadHQsFAib6uXOzKFxY8QOWHu9LCCksOINMC+TjgaKS2Tdrw7dW5SY0LLz8YK2/YqLSi2vigQt05w=="], - - "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], - - "multipasta": ["multipasta@0.2.9", "", {}, "sha512-kAovZHgxryqb0xTFz7OTCPxbas8UBTTwCDhfrlaNecp1/z6xcMfp5PVhDxsW7jONHyDF10/ccp30MxA2+66GJw=="], - "nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="], "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], @@ -689,8 +669,6 @@ "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], "npm-bundled": ["npm-bundled@5.0.0", "", { "dependencies": { "npm-normalize-package-bin": "^5.0.0" } }, "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw=="], @@ -765,8 +743,6 @@ "promise-call-limit": ["promise-call-limit@3.0.2", "", {}, "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw=="], - "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], - "read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="], "redis": ["redis@6.3.0", "", { "dependencies": { "@redis/bloom": "6.3.0", "@redis/client": "6.3.0", "@redis/json": "6.3.0", "@redis/search": "6.3.0", "@redis/time-series": "6.3.0" } }, "sha512-XFQbPie1lGpKeUZ8ySYY43yLnQy/iIQtHNvIr/1XdaABpb96rUSdm3Mfl+98FEVbv9ZJIzINMno5s0Nh8E5LEw=="], @@ -827,8 +803,6 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="], @@ -851,8 +825,6 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@14.0.3", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw=="], - "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], @@ -883,17 +855,9 @@ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], - "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], - "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - - "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - - "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="], "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -907,12 +871,6 @@ "@modelcontextprotocol/core/zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], - "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], - - "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], - - "@opencode-ai/plugin/effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="], - "@opencode/simulation/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], @@ -935,8 +893,6 @@ "node-gyp/undici": ["undici@6.29.1", "", {}, "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA=="], - "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], diff --git a/adapters/opencode-sdk/package.json b/adapters/opencode-sdk/package.json index 375642ba..dbc58d2f 100644 --- a/adapters/opencode-sdk/package.json +++ b/adapters/opencode-sdk/package.json @@ -24,5 +24,9 @@ "effect": "4.0.1", "jsonc-parser": "3.3.1", "@opencode/cli": "2.1.4" + }, + "overrides": { + "@ai-sdk/provider-utils": "4.0.33", + "effect": "4.0.1" } } From 7982c2a2d5d13e3356ad6e4c6acadd6e9bd25e68 Mon Sep 17 00:00:00 2001 From: cskwork <76669236+cskwork@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:41:39 +0900 Subject: [PATCH 6/6] fix(release): include OpenCode SDK bridge in archives --- build.sh | 3 +++ e2e/package-notices.sh | 23 +++++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/build.sh b/build.sh index be0789de..1a3c4e7d 100755 --- a/build.sh +++ b/build.sh @@ -283,6 +283,9 @@ package_binary() { mkdir -p "$package_root/adapters/agent" cp adapters/agent/package.json adapters/agent/package-lock.json adapters/agent/README.md adapters/agent/pi-extension.js "$package_root/adapters/agent/" cp -R adapters/agent/src adapters/agent/test "$package_root/adapters/agent/" + mkdir -p "$package_root/adapters/opencode-sdk" + cp adapters/opencode-sdk/package.json adapters/opencode-sdk/bun.lock adapters/opencode-sdk/README*.md "$package_root/adapters/opencode-sdk/" + cp -R adapters/opencode-sdk/src adapters/opencode-sdk/test "$package_root/adapters/opencode-sdk/" mkdir -p "$package_root/log" cp log/changelog-*.md "$package_root/log/" (cd "$ARTEX_PACKAGE_DIR" && zip -q -r -9 "$(basename "$archive")" "$(basename "$package_root")") diff --git a/e2e/package-notices.sh b/e2e/package-notices.sh index 69998b37..0bdc3099 100644 --- a/e2e/package-notices.sh +++ b/e2e/package-notices.sh @@ -14,9 +14,12 @@ trap cleanup EXIT export PACKAGE_FIXTURE_PYTHON="$(command -v python || command -v python3)" cp "$repo/build.sh" "$repo/THIRD_PARTY_NOTICES.txt" "$fixture/" mkdir -p "$fixture/bin" "$fixture/server/webui/dist" "$fixture/skills" "$fixture/docs" "$fixture/log" \ - "$fixture/adapters/agent/src" "$fixture/adapters/agent/test" + "$fixture/adapters/agent/src" "$fixture/adapters/agent/test" \ + "$fixture/adapters/opencode-sdk/src" "$fixture/adapters/opencode-sdk/test" for name in start.sh start.bat config.example.json README.md README.ko.md LICENSE CHANGELOG.md \ adapters/agent/package.json adapters/agent/package-lock.json adapters/agent/README.md adapters/agent/pi-extension.js \ + adapters/opencode-sdk/package.json adapters/opencode-sdk/bun.lock \ + adapters/opencode-sdk/src/fixture.mjs adapters/opencode-sdk/test/fixture.mjs \ log/changelog-v0.2.1.md skills/SKILL.md docs/guide.md adapters/agent/src/fixture.js adapters/agent/test/fixture.js; do printf 'fixture %s\n' "$name" > "$fixture/$name" done @@ -34,6 +37,7 @@ documents.update(path.relative_to(repo) for path in (repo / "web/src/i18n").glob for language in languages: suffix = "" if language == "en" else f".{language}" documents.update(Path(f"{name}{suffix}.md") for name in ("README", "CONTRIBUTING")) + documents.add(Path("adapters/opencode-sdk") / f"README{suffix}.md") if language != "en": for guide in (repo / "docs/ko").rglob("*.md"): documents.add(Path("docs") / language / guide.relative_to(repo / "docs/ko")) @@ -44,7 +48,10 @@ for document in sorted(documents): target.write_text(f"fixture {document.as_posix()}\n") (fixture / "expected-documents.txt").write_text("\n".join(path.as_posix() for path in sorted(documents))) (fixture / "languages.txt").write_text(" ".join(languages)) -for name in ("sidequestion/fixture.go", "web/src/i18n/fixture.ts", "web/node_modules/fixture.js"): +for name in ("sidequestion/fixture.go", "web/src/i18n/fixture.ts", "web/node_modules/fixture.js", + "adapters/opencode-sdk/node_modules/fixture.js", "adapters/opencode-sdk/.env", + "adapters/opencode-sdk/.env.local", "adapters/opencode-sdk/provider-config.local.json", + "adapters/opencode-sdk/.runtime/fixture.json", "adapters/opencode-sdk/.cache/fixture.json"): target = fixture / name target.parent.mkdir(parents=True, exist_ok=True) target.write_text("source-only fixture\n") @@ -95,7 +102,15 @@ for path in archives: member = f"{root}/{document}" assert member in archive.namelist(), f"Missing archived document: {document}" assert archive.read(member) == f"fixture {document}\n".encode() - for name in ("sidequestion/fixture.go", "web/src/i18n/fixture.ts", "web/node_modules/fixture.js"): + for name in ("adapters/opencode-sdk/package.json", "adapters/opencode-sdk/bun.lock", + "adapters/opencode-sdk/src/fixture.mjs", "adapters/opencode-sdk/test/fixture.mjs"): + member = f"{root}/{name}" + assert member in archive.namelist(), f"Missing SDK adapter payload: {name}" + assert archive.read(member) == f"fixture {name}\n".encode() + for name in ("sidequestion/fixture.go", "web/src/i18n/fixture.ts", "web/node_modules/fixture.js", + "adapters/opencode-sdk/node_modules/fixture.js", "adapters/opencode-sdk/.env", + "adapters/opencode-sdk/.env.local", "adapters/opencode-sdk/provider-config.local.json", + "adapters/opencode-sdk/.runtime/fixture.json", "adapters/opencode-sdk/.cache/fixture.json"): assert f"{root}/{name}" not in archive.namelist(), f"Unexpected archived source: {name}" target = root.removeprefix("scopeweaver-0.2.1-").replace("-", "/") binary = "scopeweaver.exe" if target.startswith("windows/") else "scopeweaver" @@ -104,6 +119,6 @@ for line in (directory / "SHA256SUMS").read_text().splitlines(): digest, name = line.split() name = name.removeprefix("*") assert hashlib.sha256((directory / name).read_bytes()).hexdigest() == digest -print("PASS: all five archives contain exact notice and unchanged fixture executables") +print(f"PASS: {directory.name.removeprefix('dist-')}: all five archives contain exact notices, SDK adapter payload and unchanged fixture executables without local dependencies/configuration") PY done