mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 248a45801d | |||
| df3895d74f | |||
| 55e0af1405 | |||
| 5182a3698d | |||
| 73e1de4513 | |||
| 44b432c3fd | |||
| 0d8c9f3437 | |||
| 0d074492df |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -119,7 +119,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -146,7 +146,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -168,7 +168,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -192,7 +192,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -253,7 +253,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -307,7 +307,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -337,7 +337,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -353,7 +353,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -366,7 +366,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -384,7 +384,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -520,7 +520,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -558,7 +558,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -573,7 +573,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -608,7 +608,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -657,7 +657,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-cRhvzZoW6gBbE0sQm1+e+6/WgajuA6MSIL5iroFsfqs=",
|
||||
"aarch64-linux": "sha256-0knZfxBULqkt5u6sXFx+a/vqw2rc6IC1+LeAd4TNFhM=",
|
||||
"aarch64-darwin": "sha256-jL4tO+EHSmUF+gQGEaLzAbTxxjkL8OyhTk13vsbomgM=",
|
||||
"x86_64-darwin": "sha256-bsa7IpS3GaxagcigTa0yqZTkf4e/nbcTQ9aZeb+5eHQ="
|
||||
"x86_64-linux": "sha256-QIj9PhOXR/GngV/dPjCF7n5rKro2fcTNGzJ47a41Z2Q=",
|
||||
"aarch64-linux": "sha256-fQl7BjjTYtRKT3HRVhubaIVww/puUFSTzVV5bTy8II8=",
|
||||
"aarch64-darwin": "sha256-81IAmdjiYZz8IgMJt0+VxzdOS80gTHc5SendwEW/vD4=",
|
||||
"x86_64-darwin": "sha256-5OMX4VVBMfEmkYvzd09oksAt5hKkxDs84miO804LBI8="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -216,7 +216,7 @@ export async function handler(
|
||||
// ie. 400 error is usually provider error like malformed request
|
||||
res.status !== 400 &&
|
||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||
res.status !== 404 &&
|
||||
!(modelInfo.id.startsWith("gpt-") && res.status === 404) &&
|
||||
// ie. cannot change codex model providers mid-session
|
||||
modelInfo.stickyProvider !== "strict" &&
|
||||
modelInfo.fallbackProvider &&
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
export * as LayerMapExample from "./layer-map.example"
|
||||
|
||||
import { Context, Effect, Layer, LayerMap } from "effect"
|
||||
import { Npm } from "../npm"
|
||||
|
||||
/**
|
||||
* Tutorial: split global services from context-specific services.
|
||||
*
|
||||
* Use this pattern when part of the app should be constructed once at the app edge,
|
||||
* while another part should be cached per request/project/workspace key.
|
||||
*
|
||||
* In this example:
|
||||
* - Npm.Service is the global service. It is not keyed by request context and should
|
||||
* be provided once by the application runtime.
|
||||
* - ConfigService is context-specific. It is built from a RequestContext key and is
|
||||
* cached by LayerMap for that key.
|
||||
* - ConfigServiceMap.layer owns the cache. Provide it once globally, then each
|
||||
* request can provide ConfigServiceMap.get(context) to select the right instance.
|
||||
*
|
||||
* Lifetime model:
|
||||
* - ConfigServiceMap.layer has the app/global lifetime and depends on Npm.Service.
|
||||
* - ConfigServiceMap.get(context) has the request/context lifetime and provides
|
||||
* ConfigService for exactly that context key.
|
||||
* - The cached ConfigService entry stays alive while something is using it. Once idle,
|
||||
* it remains cached for idleTimeToLive, then its scope is finalized.
|
||||
* - invalidate(context) removes the cache entry for future lookups. Active users keep
|
||||
* running on the old instance; the next lookup can create a fresh instance.
|
||||
*
|
||||
* Key model:
|
||||
* - Keys can be strings, structs, classes, arrays, etc.
|
||||
* - Prefer primitive or immutable keys. Effect uses Hash / Equal semantics for cache
|
||||
* lookup, so mutating an object after it has been used as a key is a bug.
|
||||
*/
|
||||
|
||||
export type RequestContext = {
|
||||
readonly directory: string
|
||||
readonly workspace: string
|
||||
}
|
||||
|
||||
export class RequestContextRef extends Context.Service<RequestContextRef, RequestContext>()(
|
||||
"@opencode/example/RequestContextRef",
|
||||
) {}
|
||||
|
||||
export interface ConfigServiceShape {
|
||||
readonly directory: string
|
||||
readonly workspace: string
|
||||
readonly nextUse: () => Effect.Effect<number>
|
||||
readonly which: Npm.Interface["which"]
|
||||
}
|
||||
|
||||
export class ConfigService extends Context.Service<ConfigService, ConfigServiceShape>()(
|
||||
"@opencode/example/ConfigService",
|
||||
) {}
|
||||
|
||||
const configServiceLayer = Layer.effect(
|
||||
ConfigService,
|
||||
Effect.gen(function* () {
|
||||
const context = yield* RequestContextRef
|
||||
const npm = yield* Npm.Service
|
||||
|
||||
let useCount = 0
|
||||
|
||||
return ConfigService.of({
|
||||
directory: context.directory,
|
||||
workspace: context.workspace,
|
||||
nextUse: () => Effect.succeed(++useCount),
|
||||
which: npm.which,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export class ConfigServiceMap extends LayerMap.Service<ConfigServiceMap>()("@opencode/example/ConfigServiceMap", {
|
||||
lookup: (context: RequestContext) =>
|
||||
configServiceLayer.pipe(Layer.provide(Layer.succeed(RequestContextRef, RequestContextRef.of(context)))),
|
||||
idleTimeToLive: "5 minutes",
|
||||
}) {}
|
||||
|
||||
export const appLayer = ConfigServiceMap.layer
|
||||
|
||||
export const readConfig = Effect.fn("LayerMapExample.readConfig")(function* () {
|
||||
const config = yield* ConfigService
|
||||
|
||||
return {
|
||||
directory: config.directory,
|
||||
workspace: config.workspace,
|
||||
useCount: yield* config.nextUse(),
|
||||
}
|
||||
})
|
||||
|
||||
export const handleRequest = Effect.fn("LayerMapExample.handleRequest")(function* (context: RequestContext) {
|
||||
return yield* readConfig().pipe(Effect.provide(ConfigServiceMap.get(context)))
|
||||
})
|
||||
|
||||
export const invalidateContext = (context: RequestContext) => ConfigServiceMap.invalidate(context)
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.14.48"
|
||||
version = "1.14.49"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.48/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.49/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.48/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.49/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.48/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.49/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.48/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.49/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.48/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.49/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
# Error Boundaries Plan
|
||||
|
||||
Plan for removing `NamedError` as connective tissue while keeping public
|
||||
wire contracts stable.
|
||||
|
||||
## Desired Shape
|
||||
|
||||
```text
|
||||
Domain/service error
|
||||
Schema.TaggedErrorClass
|
||||
- catchable with catchTag / catchTags
|
||||
- appears in service method error type
|
||||
- no HTTP status
|
||||
- no toObject()
|
||||
|
||||
HTTP public error
|
||||
Schema.ErrorClass / TaggedErrorClass with httpApiStatus
|
||||
- endpoint-declared public contract
|
||||
- owns legacy { name, data } only when that is the SDK wire shape
|
||||
|
||||
CLI/user rendering
|
||||
FormatError and small format helpers
|
||||
- converts domain errors to text
|
||||
- preserves useful structured fields
|
||||
|
||||
Session/model-visible error
|
||||
first-class session/message error schema or helper
|
||||
- owns { name, data } event/message shape
|
||||
- not a service error class
|
||||
```
|
||||
|
||||
The important rule: a service error should not also be the HTTP body, CLI
|
||||
formatter, and session event body. Each seam adapts the error into the
|
||||
shape it owns.
|
||||
|
||||
## Concrete Example: Provider Model Not Found
|
||||
|
||||
Before:
|
||||
|
||||
```ts
|
||||
export const ModelNotFoundError = NamedError.create("ProviderModelNotFoundError", {
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
suggestions: Schema.optional(Schema.Array(Schema.String)),
|
||||
})
|
||||
```
|
||||
|
||||
Problems:
|
||||
|
||||
- Throwing it inside `Effect.fn` made it behave like a defect unless a
|
||||
compatibility bridge caught it.
|
||||
- HTTP middleware knew that this one domain error should be a `400`.
|
||||
- Callers read `.data.*`, which couples them to the legacy `{ name, data }`
|
||||
wire shape.
|
||||
|
||||
After:
|
||||
|
||||
```ts
|
||||
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("ProviderModelNotFoundError", {
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
suggestions: Schema.optional(Schema.Array(Schema.String)),
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model, ModelNotFoundError>
|
||||
}
|
||||
```
|
||||
|
||||
Boundary adapters:
|
||||
|
||||
```text
|
||||
CLI
|
||||
└─ FormatError sees _tag ProviderModelNotFoundError -> nice text
|
||||
|
||||
Session prompt
|
||||
└─ catch ModelNotFoundError -> publish Session.Event.Error as message/session wire shape
|
||||
|
||||
HTTP route
|
||||
└─ catch ModelNotFoundError -> declared BadRequest public API error when the endpoint needs it
|
||||
|
||||
HTTP middleware
|
||||
└─ no Provider.ModelNotFoundError knowledge
|
||||
```
|
||||
|
||||
## Refining Known Promise Failures
|
||||
|
||||
Use `EffectPromise.refineRejection(...)` when a Promise boundary can reject
|
||||
with many unknown values, but only one or two rejection classes are expected
|
||||
domain failures. Unknown rejections stay defects; the helper maps only known
|
||||
rejection shapes to typed errors.
|
||||
|
||||
```ts
|
||||
const language =
|
||||
yield *
|
||||
EffectPromise.refineRejection(
|
||||
async () => loadFromProvider(),
|
||||
(cause) => (cause instanceof NoSuchModelError ? new ModelNotFoundError({ providerID, modelID, cause }) : undefined),
|
||||
)
|
||||
```
|
||||
|
||||
Use this when the Promise can genuinely reject and most rejection values are
|
||||
still defects for the current module. Use `Effect.tryPromise({ try, catch })`
|
||||
when every rejection should become the same expected error type. Use
|
||||
`Effect.promise(...)` only when rejection means a defect and you do not need
|
||||
to refine known rejection classes.
|
||||
|
||||
## Helper Modules We Probably Want
|
||||
|
||||
Add helpers only when repeated call sites prove the seam is real.
|
||||
|
||||
### HTTP API Errors
|
||||
|
||||
Likely location: `src/server/routes/instance/httpapi/errors.ts`.
|
||||
|
||||
Purpose:
|
||||
|
||||
- construct public HTTP error bodies
|
||||
- preserve legacy `{ name, data }` where needed
|
||||
- attach `httpApiStatus`
|
||||
|
||||
Good helpers:
|
||||
|
||||
```ts
|
||||
notFound(message)
|
||||
badRequest(message)
|
||||
unknown()
|
||||
```
|
||||
|
||||
Avoid:
|
||||
|
||||
```ts
|
||||
mapAnyDomainError(error)
|
||||
```
|
||||
|
||||
That recreates the giant middleware mapper problem.
|
||||
|
||||
### Session / Message Error Wire Helpers
|
||||
|
||||
Likely location: near `src/session/message-error.ts` or a new narrow
|
||||
module such as `src/session/event-error.ts`.
|
||||
|
||||
Purpose:
|
||||
|
||||
- construct the `{ name, data }` shape used by `Session.Event.Error` and
|
||||
assistant message errors
|
||||
- replace `new NamedError.Unknown(...).toObject()` call sites
|
||||
- keep model-visible error bodies separate from service/domain errors
|
||||
|
||||
Good helpers:
|
||||
|
||||
```ts
|
||||
unknown(message)
|
||||
agentNotFound(agent, available)
|
||||
commandNotFound(command, available)
|
||||
modelNotFound(error: Provider.ModelNotFoundError)
|
||||
```
|
||||
|
||||
### CLI Formatters
|
||||
|
||||
Likely location: `src/cli/error.ts` until repetition demands domain-local
|
||||
format helpers.
|
||||
|
||||
Purpose:
|
||||
|
||||
- produce human-readable terminal messages from typed errors
|
||||
- support old `{ name, data }` shapes only while compatibility is needed
|
||||
|
||||
## Migration Queue
|
||||
|
||||
### Remove Domain Knowledge From HTTP Middleware
|
||||
|
||||
- [x] Storage not found no longer maps through defect fallback.
|
||||
- [x] Worktree expected errors moved to typed errors.
|
||||
- [x] Provider auth expected errors moved to typed errors.
|
||||
- [x] Provider model not found no longer needs an HTTP middleware status
|
||||
special case.
|
||||
- [ ] Convert `Session.BusyError` and map it at route boundaries.
|
||||
- [ ] Delete the broad `NamedError` middleware branch once no route relies
|
||||
on defect-wrapped legacy domain errors.
|
||||
- [ ] Keep one final unknown-defect fallback that logs `Cause.pretty(cause)`
|
||||
and returns a safe `500` body.
|
||||
|
||||
### Remaining `NamedError.create(...)` Service Errors
|
||||
|
||||
These should become `Schema.TaggedErrorClass` when touched:
|
||||
|
||||
- [ ] `src/provider/provider.ts` — `ProviderInitError`.
|
||||
- [ ] `src/storage/db.ts` — database `NotFoundError`.
|
||||
- [ ] `src/mcp/index.ts` — `MCPFailed`.
|
||||
- [ ] `src/skill/index.ts` — `SkillInvalidError`,
|
||||
`SkillNameMismatchError`.
|
||||
- [ ] `src/lsp/client.ts` — `LSPInitializeError`.
|
||||
- [ ] `src/ide/index.ts` — install errors.
|
||||
- [ ] `src/config/error.ts`, `src/config/config.ts`,
|
||||
`src/config/markdown.ts` — config errors. These already render well
|
||||
in the CLI, so migrate carefully and preserve diagnostics.
|
||||
|
||||
### Session / Message Wire Errors
|
||||
|
||||
These are not ordinary service errors. They mostly build `{ name, data }`
|
||||
objects for model-visible/session-visible output.
|
||||
|
||||
- [ ] Add a first-class session/message error wire helper.
|
||||
- [ ] Replace `new NamedError.Unknown(...).toObject()` in
|
||||
`src/session/prompt.ts`.
|
||||
- [ ] Replace `new NamedError.Unknown(...).toObject()` in config/skill/plugin
|
||||
session event publishing.
|
||||
- [ ] Move `src/session/message-error.ts` and `src/session/message-v2.ts`
|
||||
away from `NamedError.create(...)` once the wire helper exists.
|
||||
- [ ] Update retry/message tests to assert the wire schema/helper output,
|
||||
not `NamedError` instances.
|
||||
|
||||
### CLI Rendering
|
||||
|
||||
- [x] Tagged config errors render with useful diagnostics.
|
||||
- [x] Provider model not found renders from both old `{ name, data }` and
|
||||
new `_tag` shapes.
|
||||
- [ ] Add typed render cases as more `NamedError.create(...)` domains move
|
||||
to `Schema.TaggedErrorClass`.
|
||||
- [ ] Eventually remove old-shape compatibility branches when no callers can
|
||||
produce them.
|
||||
|
||||
## PR Checklist
|
||||
|
||||
For each migrated error:
|
||||
|
||||
- [ ] Domain error is `Schema.TaggedErrorClass`.
|
||||
- [ ] Service method exposes the typed error in its error channel.
|
||||
- [ ] No service error has `toObject()` just for compatibility.
|
||||
- [ ] CLI, HTTP, and session/message adapters each own their output shape.
|
||||
- [ ] HTTP middleware gets smaller or stays unchanged.
|
||||
- [ ] Focused tests cover the domain error and any public rendering/wire
|
||||
shape touched by the PR.
|
||||
@@ -4,6 +4,9 @@ This note expands the `ERR`, `RENDER`, and `HTTP` tracks from
|
||||
[`todo.md`](./todo.md). It is the current reference for expected failures,
|
||||
typed service errors, and HTTP error boundaries.
|
||||
|
||||
For the migration architecture and queue, see
|
||||
[`error-boundaries-plan.md`](./error-boundaries-plan.md).
|
||||
|
||||
## Goal
|
||||
|
||||
- Expected service failures live on the Effect error channel.
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
Short roadmap for Effect cleanup in `packages/opencode`.
|
||||
|
||||
Current patterns and examples live in [`guide.md`](./guide.md). Test
|
||||
migration rules live in
|
||||
Current patterns and examples live in [`guide.md`](./guide.md). Error
|
||||
boundary migration details live in
|
||||
[`error-boundaries-plan.md`](./error-boundaries-plan.md). Test migration rules live in
|
||||
[`test/EFFECT_TEST_MIGRATION.md`](../../test/EFFECT_TEST_MIGRATION.md).
|
||||
Older deep-dive notes in this directory may still be useful, but treat
|
||||
this roadmap and the guide as the current entry points.
|
||||
|
||||
@@ -62,11 +62,14 @@ export interface Interface {
|
||||
readonly generate: (input: {
|
||||
description: string
|
||||
model?: { providerID: ProviderID; modelID: ModelID }
|
||||
}) => Effect.Effect<{
|
||||
identifier: string
|
||||
whenToUse: string
|
||||
systemPrompt: string
|
||||
}>
|
||||
}) => Effect.Effect<
|
||||
{
|
||||
identifier: string
|
||||
whenToUse: string
|
||||
systemPrompt: string
|
||||
},
|
||||
Provider.ModelNotFoundError
|
||||
>
|
||||
}
|
||||
|
||||
type State = Omit<Interface, "generate">
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
// local sessions,
|
||||
// 4. runs the prompt queue until the footer closes.
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { createRunDemo } from "./demo"
|
||||
import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo, resolveSessionInfo } from "./runtime.boot"
|
||||
import { createRuntimeLifecycle } from "./runtime.lifecycle"
|
||||
@@ -70,9 +72,10 @@ type RunLocalInput = {
|
||||
demo?: RunInput["demo"]
|
||||
}
|
||||
|
||||
type StreamModule = Awaited<typeof import("./stream.transport")>
|
||||
type StreamState = {
|
||||
mod: Awaited<typeof import("./stream.transport")>
|
||||
handle: Awaited<ReturnType<Awaited<typeof import("./stream.transport")>["createSessionTransport"]>>
|
||||
mod: StreamModule
|
||||
handle: import("./stream.transport").SessionTransport
|
||||
}
|
||||
|
||||
type ResolvedSession = {
|
||||
@@ -485,15 +488,27 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
throw new Error("runtime closed")
|
||||
}
|
||||
|
||||
const handle = await mod.createSessionTransport({
|
||||
sdk: ctx.sdk,
|
||||
directory: ctx.directory,
|
||||
sessionID: state.sessionID,
|
||||
thinking: input.thinking,
|
||||
limits: () => state.limits,
|
||||
footer,
|
||||
trace: log,
|
||||
})
|
||||
// Yield SessionTransport.Service inside Effect.gen and call svc.make.
|
||||
// The handle holds its own internal scope; teardown happens via handle.close().
|
||||
const inner = await AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* mod.SessionTransport.Service
|
||||
return yield* svc.make({
|
||||
sdk: ctx.sdk,
|
||||
directory: ctx.directory,
|
||||
sessionID: state.sessionID,
|
||||
thinking: input.thinking,
|
||||
limits: () => state.limits,
|
||||
footer,
|
||||
trace: log,
|
||||
})
|
||||
}).pipe(Effect.provide(mod.SessionTransport.layer)),
|
||||
)
|
||||
const handle: import("./stream.transport").SessionTransport = {
|
||||
runPromptTurn: (next) => AppRuntime.runPromise(inner.runPromptTurn(next)),
|
||||
selectSubagent: (sessionID) => AppRuntime.runSync(inner.selectSubagent(sessionID)),
|
||||
close: () => AppRuntime.runPromise(inner.close()),
|
||||
}
|
||||
if (footer.isClosed) {
|
||||
await handle.close()
|
||||
throw new Error("runtime closed")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { errorFormat } from "@/util/error"
|
||||
import { isRecord } from "@/util/record"
|
||||
|
||||
interface ErrorLike {
|
||||
name?: string
|
||||
@@ -10,10 +11,6 @@ interface ErrorLike {
|
||||
|
||||
type ConfigIssue = { message: string; path: string[] }
|
||||
|
||||
function isRecord(input: unknown): input is Record<string, unknown> {
|
||||
return typeof input === "object" && input !== null
|
||||
}
|
||||
|
||||
function isTaggedError(error: unknown, tag: string): boolean {
|
||||
return isRecord(error) && error._tag === tag
|
||||
}
|
||||
@@ -61,11 +58,13 @@ export function FormatError(input: unknown) {
|
||||
}
|
||||
|
||||
// ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] }
|
||||
if (NamedError.hasName(input, "ProviderModelNotFoundError")) {
|
||||
const data = (input as ErrorLike).data
|
||||
const suggestions = Array.isArray(data?.suggestions) ? data.suggestions.filter((x) => typeof x === "string") : []
|
||||
const providerModelNotFound = configData(input, "ProviderModelNotFoundError")
|
||||
if (providerModelNotFound) {
|
||||
const suggestions = Array.isArray(providerModelNotFound.suggestions)
|
||||
? providerModelNotFound.suggestions.filter((x) => typeof x === "string")
|
||||
: []
|
||||
return [
|
||||
`Model not found: ${data?.providerID}/${data?.modelID}`,
|
||||
`Model not found: ${providerModelNotFound.providerID}/${providerModelNotFound.modelID}`,
|
||||
...(suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
|
||||
`Try: \`opencode models\` to list available models`,
|
||||
`Or check your config (opencode.json) provider/model names`,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Cause, Effect } from "effect"
|
||||
|
||||
export function refineRejection<A, E>(
|
||||
evaluate: (signal: AbortSignal) => PromiseLike<A>,
|
||||
refine: (cause: unknown) => E | undefined,
|
||||
) {
|
||||
return Effect.tryPromise(evaluate).pipe(
|
||||
Effect.catch((error) => {
|
||||
const cause = Cause.isUnknownError(error) ? error.cause : error
|
||||
const refined = refine(cause)
|
||||
if (refined !== undefined) return Effect.fail(refined)
|
||||
return Effect.die(cause)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export * as EffectPromise from "./promise"
|
||||
@@ -21,6 +21,7 @@ import { pathToFileURL } from "url"
|
||||
import { Effect, Layer, Context, Schema, Types } from "effect"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { EffectPromise } from "@/effect/promise"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
@@ -963,11 +964,24 @@ export function defaultModelIDs<T extends { models: Record<string, { id: string
|
||||
return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
|
||||
}
|
||||
|
||||
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("ProviderModelNotFoundError", {
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
suggestions: Schema.optional(Schema.Array(Schema.String)),
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {
|
||||
static isInstance(input: unknown): input is ModelNotFoundError {
|
||||
return input instanceof ModelNotFoundError
|
||||
}
|
||||
}
|
||||
|
||||
export type Error = ModelNotFoundError
|
||||
|
||||
export interface Interface {
|
||||
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
|
||||
readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
|
||||
readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model>
|
||||
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3>
|
||||
readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model, ModelNotFoundError>
|
||||
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
|
||||
readonly closest: (
|
||||
providerID: ProviderID,
|
||||
query: string[],
|
||||
@@ -1638,14 +1652,14 @@ const layer = Layer.effect(
|
||||
: fuzzysort
|
||||
.go(providerID, Object.keys({ ...s.catalog, ...s.providers }), { limit: 3, threshold: -10000 })
|
||||
.map((m) => m.target)
|
||||
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||
return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||
}
|
||||
|
||||
const info = provider.models[modelID]
|
||||
if (!info) {
|
||||
const current = modelSuggestions(provider, modelID)
|
||||
const suggestions = current.length ? current : modelSuggestions(s.catalog[providerID], modelID)
|
||||
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||
return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||
}
|
||||
return info
|
||||
})
|
||||
@@ -1656,11 +1670,10 @@ const layer = Layer.effect(
|
||||
const key = `${model.providerID}/${model.id}`
|
||||
if (s.models.has(key)) return s.models.get(key)!
|
||||
|
||||
return yield* Effect.promise(async () => {
|
||||
const provider = s.providers[model.providerID]
|
||||
const sdk = await resolveSDK(model, s, envs)
|
||||
|
||||
try {
|
||||
const provider = s.providers[model.providerID]
|
||||
return yield* EffectPromise.refineRejection(
|
||||
async () => {
|
||||
const sdk = await resolveSDK(model, s, envs)
|
||||
const language = s.modelLoaders[model.providerID]
|
||||
? await s.modelLoaders[model.providerID](sdk, model.api.id, {
|
||||
...provider.options,
|
||||
@@ -1669,18 +1682,12 @@ const layer = Layer.effect(
|
||||
: sdk.languageModel(model.api.id)
|
||||
s.models.set(key, language)
|
||||
return language
|
||||
} catch (e) {
|
||||
if (e instanceof NoSuchModelError)
|
||||
throw new ModelNotFoundError(
|
||||
{
|
||||
modelID: model.id,
|
||||
providerID: model.providerID,
|
||||
},
|
||||
{ cause: e },
|
||||
)
|
||||
throw e
|
||||
}
|
||||
})
|
||||
},
|
||||
(cause) =>
|
||||
cause instanceof NoSuchModelError
|
||||
? new ModelNotFoundError({ modelID: model.id, providerID: model.providerID, cause })
|
||||
: undefined,
|
||||
)
|
||||
})
|
||||
|
||||
const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
|
||||
@@ -1700,7 +1707,7 @@ const layer = Layer.effect(
|
||||
|
||||
if (cfg.small_model) {
|
||||
const parsed = parseModel(cfg.small_model)
|
||||
return yield* getModel(parsed.providerID, parsed.modelID)
|
||||
return yield* getModel(parsed.providerID, parsed.modelID).pipe(Effect.orDie)
|
||||
}
|
||||
|
||||
const s = yield* InstanceState.get(state)
|
||||
@@ -1728,22 +1735,22 @@ const layer = Layer.effect(
|
||||
const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
|
||||
|
||||
const globalMatch = candidates.find((m) => m.startsWith("global."))
|
||||
if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch))
|
||||
if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch)).pipe(Effect.orDie)
|
||||
|
||||
const region = provider.options?.region
|
||||
if (region) {
|
||||
const regionPrefix = region.split("-")[0]
|
||||
if (regionPrefix === "us" || regionPrefix === "eu") {
|
||||
const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
|
||||
if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch))
|
||||
if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch)).pipe(Effect.orDie)
|
||||
}
|
||||
}
|
||||
|
||||
const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
|
||||
if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed))
|
||||
if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed)).pipe(Effect.orDie)
|
||||
} else {
|
||||
for (const model of Object.keys(provider.models)) {
|
||||
if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model))
|
||||
if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model)).pipe(Effect.orDie)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1818,12 +1825,6 @@ export function parseModel(model: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const ModelNotFoundError = NamedError.create("ProviderModelNotFoundError", {
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
suggestions: Schema.optional(Schema.Array(Schema.String)),
|
||||
})
|
||||
|
||||
export const InitError = NamedError.create("ProviderInitError", {
|
||||
providerID: ProviderID,
|
||||
})
|
||||
|
||||
@@ -617,14 +617,6 @@ function googleThinkingBudgetMax(apiId: string) {
|
||||
return 24_576
|
||||
}
|
||||
|
||||
function googleSmallThinkingConfig(apiId: string) {
|
||||
const levels = googleThinkingLevelEfforts(apiId)
|
||||
if (apiId.toLowerCase().includes("gemini-3")) {
|
||||
return { thinkingLevel: levels.includes("minimal") ? "minimal" : levels.includes("low") ? "low" : "high" }
|
||||
}
|
||||
return { thinkingBudget: googleThinkingBudgetMax(apiId) === 32_768 ? 128 : 0 }
|
||||
}
|
||||
|
||||
export function variants(model: Provider.Model): Record<string, Record<string, any>> {
|
||||
if (!model.capabilities.reasoning) return {}
|
||||
|
||||
@@ -1184,40 +1176,27 @@ export function options(input: {
|
||||
}
|
||||
|
||||
export function smallOptions(model: Provider.Model) {
|
||||
const small = Object.values(model.variants ?? {})[0] ?? {}
|
||||
if (
|
||||
model.providerID === "openai" ||
|
||||
model.api.npm === "@ai-sdk/openai" ||
|
||||
model.api.npm === "@ai-sdk/github-copilot"
|
||||
) {
|
||||
if (model.api.id.includes("gpt-5")) {
|
||||
if (model.api.id.includes("-chat")) {
|
||||
if (gpt5Version(model.api.id) === undefined) return { store: false }
|
||||
return { store: false, reasoningEffort: "medium" }
|
||||
}
|
||||
if (model.api.id.includes("search-api")) return { store: false }
|
||||
if (model.api.id.includes("5.") || model.api.id.includes("5-mini")) {
|
||||
return { store: false, reasoningEffort: "low" }
|
||||
}
|
||||
return { store: false, reasoningEffort: "minimal" }
|
||||
}
|
||||
return { store: false }
|
||||
}
|
||||
if (model.providerID === "google") {
|
||||
// gemini-3 uses thinkingLevel, gemini-2.5 uses thinkingBudget
|
||||
return { thinkingConfig: googleSmallThinkingConfig(model.api.id) }
|
||||
const base = { store: false }
|
||||
return mergeDeep(base, small)
|
||||
}
|
||||
if (model.providerID === "openrouter" || model.providerID === "llmgateway") {
|
||||
if (model.api.id.includes("google")) {
|
||||
if (Object.keys(small).length === 0 && model.api.id.includes("google")) {
|
||||
return { reasoning: { enabled: false } }
|
||||
}
|
||||
return { reasoningEffort: "minimal" }
|
||||
}
|
||||
|
||||
if (model.providerID === "venice") {
|
||||
if (Object.keys(small).length > 0) return small
|
||||
return { veniceParameters: { disableThinking: true } }
|
||||
}
|
||||
|
||||
return {}
|
||||
return small
|
||||
}
|
||||
|
||||
// Maps model ID prefix to provider slug used in providerOptions.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { iife } from "@/util/iife"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Cause, Effect } from "effect"
|
||||
@@ -23,14 +21,7 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect)
|
||||
log.error("failed", { error, cause: Cause.pretty(cause) })
|
||||
|
||||
if (error instanceof NamedError) {
|
||||
return Effect.succeed(
|
||||
HttpServerResponse.jsonUnsafe(error.toObject(), {
|
||||
status: iife(() => {
|
||||
if (error instanceof Provider.ModelNotFoundError) return 400
|
||||
return 500
|
||||
}),
|
||||
}),
|
||||
)
|
||||
return Effect.succeed(HttpServerResponse.jsonUnsafe(error.toObject(), { status: 500 }))
|
||||
}
|
||||
return Effect.succeed(
|
||||
HttpServerResponse.jsonUnsafe(
|
||||
|
||||
@@ -390,8 +390,8 @@ export const layer: Layer.Layer<
|
||||
|
||||
const agent = yield* agents.get("compaction")
|
||||
const model = agent.model
|
||||
? yield* provider.getModel(agent.model.providerID, agent.model.modelID)
|
||||
: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
|
||||
? yield* provider.getModel(agent.model.providerID, agent.model.modelID).pipe(Effect.orDie)
|
||||
: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID).pipe(Effect.orDie)
|
||||
const cfg = yield* config.get()
|
||||
const history = compactionPart && messages.at(-1)?.info.id === input.parentID ? messages.slice(0, -1) : messages
|
||||
const prior = completedCompactions(history)
|
||||
@@ -519,7 +519,9 @@ export const layer: Layer.Layer<
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
agent: userMessage.agent,
|
||||
model: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID),
|
||||
model: yield* provider
|
||||
.getModel(userMessage.model.providerID, userMessage.model.modelID)
|
||||
.pipe(Effect.orDie),
|
||||
provider: {
|
||||
source: info.source,
|
||||
info,
|
||||
|
||||
@@ -1057,15 +1057,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
if (Exit.isSuccess(exit)) return exit.value
|
||||
const err = Cause.squash(exit.cause)
|
||||
if (Provider.ModelNotFoundError.isInstance(err)) {
|
||||
const hint = err.data.suggestions?.length ? ` Did you mean: ${err.data.suggestions.join(", ")}?` : ""
|
||||
const hint = err.suggestions?.length ? ` Did you mean: ${err.suggestions.join(", ")}?` : ""
|
||||
yield* bus.publish(Session.Event.Error, {
|
||||
sessionID,
|
||||
error: new NamedError.Unknown({
|
||||
message: `Model not found: ${err.data.providerID}/${err.data.modelID}.${hint}`,
|
||||
message: `Model not found: ${err.providerID}/${err.modelID}.${hint}`,
|
||||
}).toObject(),
|
||||
})
|
||||
}
|
||||
return yield* Effect.failCause(exit.cause)
|
||||
return yield* Effect.die(err)
|
||||
})
|
||||
|
||||
const currentModel = Effect.fnUntraced(function* (sessionID: SessionID) {
|
||||
@@ -1108,7 +1108,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID
|
||||
const full =
|
||||
!input.variant && ag.variant && same
|
||||
? yield* provider.getModel(model.providerID, model.modelID).pipe(Effect.catchDefect(() => Effect.void))
|
||||
? yield* provider
|
||||
.getModel(model.providerID, model.modelID)
|
||||
.pipe(Effect.catchIf(Provider.ModelNotFoundError.isInstance, () => Effect.succeed(undefined)))
|
||||
: undefined
|
||||
const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
|
||||
|
||||
|
||||
@@ -64,6 +64,23 @@ describe("cli.error", () => {
|
||||
expect(formatted).toContain("Check your network, proxy, or VPN configuration and try again.")
|
||||
})
|
||||
|
||||
test("formats legacy and tagged provider model errors the same way", () => {
|
||||
const data = {
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-sonet-4",
|
||||
suggestions: ["claude-sonnet-4"],
|
||||
}
|
||||
const expected = [
|
||||
"Model not found: anthropic/claude-sonet-4",
|
||||
"Did you mean: claude-sonnet-4",
|
||||
"Try: `opencode models` to list available models",
|
||||
"Or check your config (opencode.json) provider/model names",
|
||||
].join("\n")
|
||||
|
||||
expect(FormatError({ name: "ProviderModelNotFoundError", data })).toBe(expected)
|
||||
expect(FormatError({ _tag: "ProviderModelNotFoundError", ...data })).toBe(expected)
|
||||
})
|
||||
|
||||
test("formats cancelled UI errors as empty output", () => {
|
||||
expect(FormatError(new UI.CancelledError())).toBe("")
|
||||
})
|
||||
|
||||
@@ -749,30 +749,39 @@ describe("workspace CRUD", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("remove deletes the workspace, associated sessions, adapter resources, and status", async () => {
|
||||
await withInstance(async (dir) => {
|
||||
const type = unique("remove-local")
|
||||
const recorded = localAdapter(path.join(dir, "remove-local"))
|
||||
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||
const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null })
|
||||
const one = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||
const two = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||
attachSessionToWorkspace(one.id, info.id)
|
||||
attachSessionToWorkspace(two.id, info.id)
|
||||
it.instance(
|
||||
"remove deletes the workspace, associated sessions, adapter resources, and status",
|
||||
() => {
|
||||
return Effect.gen(function* () {
|
||||
const { directory: dir } = yield* TestInstance
|
||||
const instance = yield* InstanceRef
|
||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
||||
const workspace = yield* Workspace.Service
|
||||
const sessionSvc = yield* SessionNs.Service
|
||||
const type = unique("remove-local")
|
||||
const recorded = localAdapter(path.join(dir, "remove-local"))
|
||||
registerAdapter(instance.project.id, type, recorded.adapter)
|
||||
const info = yield* workspace.create({ type, branch: null, projectID: instance.project.id, extra: null })
|
||||
const one = yield* sessionSvc.create({})
|
||||
const two = yield* sessionSvc.create({})
|
||||
attachSessionToWorkspace(one.id, info.id)
|
||||
attachSessionToWorkspace(two.id, info.id)
|
||||
|
||||
const removed = await removeWorkspace(info.id)
|
||||
const removed = yield* workspace.remove(info.id)
|
||||
|
||||
expect(removed).toEqual(info)
|
||||
expect(await getWorkspace(info.id)).toBeUndefined()
|
||||
expect(recorded.calls.remove).toEqual([info])
|
||||
expect((await workspaceStatus()).find((item) => item.workspaceID === info.id)?.status).toBeUndefined()
|
||||
expect(
|
||||
Database.use((db) =>
|
||||
db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, info.id)).all(),
|
||||
),
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
expect(removed).toEqual(info)
|
||||
expect(yield* workspace.get(info.id)).toBeUndefined()
|
||||
expect(recorded.calls.remove).toEqual([info])
|
||||
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBeUndefined()
|
||||
expect(
|
||||
Database.use((db) =>
|
||||
db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, info.id)).all(),
|
||||
),
|
||||
).toEqual([])
|
||||
})
|
||||
},
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
test("remove still deletes the row when the adapter cannot remove resources", async () => {
|
||||
await withInstance(async () => {
|
||||
@@ -797,84 +806,111 @@ describe("workspace CRUD", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("sessionWarp moves a session into a local workspace and claims ownership", async () => {
|
||||
await withInstance(async (dir) => {
|
||||
const previousType = unique("warp-prev-local")
|
||||
const targetType = unique("warp-target-local")
|
||||
const previous = workspaceInfo(Instance.project.id, previousType)
|
||||
const target = workspaceInfo(Instance.project.id, targetType)
|
||||
insertWorkspace(previous)
|
||||
insertWorkspace(target)
|
||||
registerAdapter(Instance.project.id, previousType, localAdapter(path.join(dir, "warp-prev-local")).adapter)
|
||||
registerAdapter(Instance.project.id, targetType, localAdapter(path.join(dir, "warp-target-local")).adapter)
|
||||
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||
attachSessionToWorkspace(session.id, previous.id)
|
||||
it.instance(
|
||||
"sessionWarp moves a session into a local workspace and claims ownership",
|
||||
() => {
|
||||
return Effect.gen(function* () {
|
||||
const { directory: dir } = yield* TestInstance
|
||||
const instance = yield* InstanceRef
|
||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
||||
const workspace = yield* Workspace.Service
|
||||
const sessionSvc = yield* SessionNs.Service
|
||||
const previousType = unique("warp-prev-local")
|
||||
const targetType = unique("warp-target-local")
|
||||
const previous = workspaceInfo(instance.project.id, previousType)
|
||||
const target = workspaceInfo(instance.project.id, targetType)
|
||||
insertWorkspace(previous)
|
||||
insertWorkspace(target)
|
||||
registerAdapter(instance.project.id, previousType, localAdapter(path.join(dir, "warp-prev-local")).adapter)
|
||||
registerAdapter(instance.project.id, targetType, localAdapter(path.join(dir, "warp-target-local")).adapter)
|
||||
const session = yield* sessionSvc.create({})
|
||||
attachSessionToWorkspace(session.id, previous.id)
|
||||
|
||||
await warpWorkspaceSession({ workspaceID: target.id, sessionID: session.id })
|
||||
yield* workspace.sessionWarp({ workspaceID: target.id, sessionID: session.id })
|
||||
|
||||
expect(
|
||||
Database.use((db) =>
|
||||
db
|
||||
.select({ workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, session.id))
|
||||
.get(),
|
||||
)?.workspaceID,
|
||||
).toBe(target.id)
|
||||
expect(sessionSequenceOwner(session.id)).toBe(target.id)
|
||||
})
|
||||
})
|
||||
expect(
|
||||
Database.use((db) =>
|
||||
db
|
||||
.select({ workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, session.id))
|
||||
.get(),
|
||||
)?.workspaceID,
|
||||
).toBe(target.id)
|
||||
expect(sessionSequenceOwner(session.id)).toBe(target.id)
|
||||
})
|
||||
},
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
test("sessionWarp applies source workspace patch to local target workspace", async () => {
|
||||
await withInstance(async (dir) => {
|
||||
const previousType = unique("warp-patch-prev-local")
|
||||
const targetType = unique("warp-patch-target-local")
|
||||
const previousDir = path.join(dir, "warp-patch-prev-local")
|
||||
const targetDir = path.join(dir, "warp-patch-target-local")
|
||||
await initGitRepo(previousDir)
|
||||
await initGitRepo(targetDir)
|
||||
await fs.writeFile(path.join(previousDir, "tracked.txt"), "changed\n")
|
||||
await fs.writeFile(path.join(previousDir, "new.txt"), "new\n")
|
||||
it.instance(
|
||||
"sessionWarp applies source workspace patch to local target workspace",
|
||||
() => {
|
||||
return Effect.gen(function* () {
|
||||
const { directory: dir } = yield* TestInstance
|
||||
const instance = yield* InstanceRef
|
||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
||||
const workspace = yield* Workspace.Service
|
||||
const sessionSvc = yield* SessionNs.Service
|
||||
const previousType = unique("warp-patch-prev-local")
|
||||
const targetType = unique("warp-patch-target-local")
|
||||
const previousDir = path.join(dir, "warp-patch-prev-local")
|
||||
const targetDir = path.join(dir, "warp-patch-target-local")
|
||||
yield* Effect.promise(() => initGitRepo(previousDir))
|
||||
yield* Effect.promise(() => initGitRepo(targetDir))
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(previousDir, "tracked.txt"), "changed\n"))
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(previousDir, "new.txt"), "new\n"))
|
||||
|
||||
const previous = workspaceInfo(Instance.project.id, previousType)
|
||||
const target = workspaceInfo(Instance.project.id, targetType)
|
||||
insertWorkspace(previous)
|
||||
insertWorkspace(target)
|
||||
registerAdapter(Instance.project.id, previousType, localAdapter(previousDir, { createDir: false }).adapter)
|
||||
registerAdapter(Instance.project.id, targetType, localAdapter(targetDir, { createDir: false }).adapter)
|
||||
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||
attachSessionToWorkspace(session.id, previous.id)
|
||||
const previous = workspaceInfo(instance.project.id, previousType)
|
||||
const target = workspaceInfo(instance.project.id, targetType)
|
||||
insertWorkspace(previous)
|
||||
insertWorkspace(target)
|
||||
registerAdapter(instance.project.id, previousType, localAdapter(previousDir, { createDir: false }).adapter)
|
||||
registerAdapter(instance.project.id, targetType, localAdapter(targetDir, { createDir: false }).adapter)
|
||||
const session = yield* sessionSvc.create({})
|
||||
attachSessionToWorkspace(session.id, previous.id)
|
||||
|
||||
await warpWorkspaceSession({ workspaceID: target.id, sessionID: session.id, copyChanges: true })
|
||||
yield* workspace.sessionWarp({ workspaceID: target.id, sessionID: session.id, copyChanges: true })
|
||||
|
||||
expect(await fs.readFile(path.join(targetDir, "tracked.txt"), "utf8")).toBe("changed\n")
|
||||
expect(await fs.readFile(path.join(targetDir, "new.txt"), "utf8")).toBe("new\n")
|
||||
})
|
||||
})
|
||||
expect(yield* Effect.promise(() => fs.readFile(path.join(targetDir, "tracked.txt"), "utf8"))).toBe("changed\n")
|
||||
expect(yield* Effect.promise(() => fs.readFile(path.join(targetDir, "new.txt"), "utf8"))).toBe("new\n")
|
||||
})
|
||||
},
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
test("sessionWarp detaches a session to the local project and claims project ownership", async () => {
|
||||
await withInstance(async (dir) => {
|
||||
const previousType = unique("warp-detach-local")
|
||||
const previous = workspaceInfo(Instance.project.id, previousType)
|
||||
insertWorkspace(previous)
|
||||
registerAdapter(Instance.project.id, previousType, localAdapter(path.join(dir, "warp-detach-local")).adapter)
|
||||
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||
attachSessionToWorkspace(session.id, previous.id)
|
||||
it.instance(
|
||||
"sessionWarp detaches a session to the local project and claims project ownership",
|
||||
() => {
|
||||
return Effect.gen(function* () {
|
||||
const { directory: dir } = yield* TestInstance
|
||||
const instance = yield* InstanceRef
|
||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
||||
const workspace = yield* Workspace.Service
|
||||
const sessionSvc = yield* SessionNs.Service
|
||||
const previousType = unique("warp-detach-local")
|
||||
const previous = workspaceInfo(instance.project.id, previousType)
|
||||
insertWorkspace(previous)
|
||||
registerAdapter(instance.project.id, previousType, localAdapter(path.join(dir, "warp-detach-local")).adapter)
|
||||
const session = yield* sessionSvc.create({})
|
||||
attachSessionToWorkspace(session.id, previous.id)
|
||||
|
||||
await warpWorkspaceSession({ workspaceID: null, sessionID: session.id })
|
||||
yield* workspace.sessionWarp({ workspaceID: null, sessionID: session.id })
|
||||
|
||||
expect(
|
||||
Database.use((db) =>
|
||||
db
|
||||
.select({ workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, session.id))
|
||||
.get(),
|
||||
)?.workspaceID,
|
||||
).toBeNull()
|
||||
expect(sessionSequenceOwner(session.id)).toBe(Instance.project.id)
|
||||
})
|
||||
})
|
||||
expect(
|
||||
Database.use((db) =>
|
||||
db
|
||||
.select({ workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, session.id))
|
||||
.get(),
|
||||
)?.workspaceID,
|
||||
).toBeNull()
|
||||
expect(sessionSequenceOwner(session.id)).toBe(instance.project.id)
|
||||
})
|
||||
},
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
test("sessionWarp detaches to the source project when invoked from a workspace instance", async () => {
|
||||
await withInstance(async () => {
|
||||
|
||||
@@ -1572,8 +1572,8 @@ test("ModelNotFoundError includes suggestions for typos", async () => {
|
||||
await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")) // typo: sonet instead of sonnet
|
||||
expect(true).toBe(false) // Should not reach here
|
||||
} catch (e: any) {
|
||||
expect(e.data.suggestions).toBeDefined()
|
||||
expect(e.data.suggestions.length).toBeGreaterThan(0)
|
||||
expect(e.suggestions).toBeDefined()
|
||||
expect(e.suggestions.length).toBeGreaterThan(0)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1598,8 +1598,8 @@ test("ModelNotFoundError for provider includes suggestions", async () => {
|
||||
await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) // typo: antropic
|
||||
expect(true).toBe(false) // Should not reach here
|
||||
} catch (e: any) {
|
||||
expect(e.data.suggestions).toBeDefined()
|
||||
expect(e.data.suggestions).toContain("anthropic")
|
||||
expect(e.suggestions).toBeDefined()
|
||||
expect(e.suggestions).toContain("anthropic")
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1625,7 +1625,7 @@ test("ModelNotFoundError suggests catalog models for unloaded providers", async
|
||||
throw new Error("expected model lookup to fail")
|
||||
} catch (e) {
|
||||
if (!Provider.ModelNotFoundError.isInstance(e)) throw e
|
||||
expect(e.data.suggestions).toContain("claude-haiku-4-5")
|
||||
expect(e.suggestions).toContain("claude-haiku-4-5")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3602,8 +3602,8 @@ describe("ProviderTransform.variants", () => {
|
||||
})
|
||||
|
||||
describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => {
|
||||
const createModel = (apiId: string) =>
|
||||
({
|
||||
const createModel = (apiId: string) => {
|
||||
const model = {
|
||||
id: `openai/${apiId}`,
|
||||
providerID: "openai",
|
||||
api: {
|
||||
@@ -3611,13 +3611,43 @@ describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => {
|
||||
url: "https://api.openai.com",
|
||||
npm: "@ai-sdk/openai",
|
||||
},
|
||||
}) as any
|
||||
capabilities: { reasoning: true },
|
||||
limit: { output: 64_000 },
|
||||
release_date: "2026-01-01",
|
||||
} as any
|
||||
model.variants = ProviderTransform.variants(model)
|
||||
return model
|
||||
}
|
||||
|
||||
for (const testCase of [
|
||||
{ id: "gpt-5-chat-latest", options: { store: false } },
|
||||
{ id: "gpt-5.1-chat-latest", options: { store: false, reasoningEffort: "medium" } },
|
||||
{ id: "gpt-5.2-chat-latest", options: { store: false, reasoningEffort: "medium" } },
|
||||
{ id: "gpt-5-search-api", options: { store: false } },
|
||||
{
|
||||
id: "gpt-5.1-chat-latest",
|
||||
options: {
|
||||
store: false,
|
||||
reasoningEffort: "medium",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "gpt-5.2-chat-latest",
|
||||
options: {
|
||||
store: false,
|
||||
reasoningEffort: "medium",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "gpt-5-search-api",
|
||||
options: {
|
||||
store: false,
|
||||
reasoningEffort: "none",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
]) {
|
||||
test(`${testCase.id} returns only supported small options`, () => {
|
||||
expect(ProviderTransform.smallOptions(createModel(testCase.id))).toEqual(testCase.options)
|
||||
@@ -3626,8 +3656,8 @@ describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => {
|
||||
})
|
||||
|
||||
describe("ProviderTransform.smallOptions - google thinking controls", () => {
|
||||
const createGoogleModel = (apiId: string) =>
|
||||
({
|
||||
const createGoogleModel = (apiId: string) => {
|
||||
const model = {
|
||||
id: `google/${apiId}`,
|
||||
providerID: "google",
|
||||
api: {
|
||||
@@ -3635,20 +3665,44 @@ describe("ProviderTransform.smallOptions - google thinking controls", () => {
|
||||
url: "https://generativelanguage.googleapis.com",
|
||||
npm: "@ai-sdk/google",
|
||||
},
|
||||
}) as any
|
||||
capabilities: { reasoning: true },
|
||||
limit: { output: 64_000 },
|
||||
} as any
|
||||
model.variants = ProviderTransform.variants(model)
|
||||
return model
|
||||
}
|
||||
|
||||
for (const testCase of [
|
||||
{ id: "gemini-3-pro-preview", options: { thinkingConfig: { thinkingLevel: "low" } } },
|
||||
{ id: "gemini-3-flash-preview", options: { thinkingConfig: { thinkingLevel: "minimal" } } },
|
||||
{ id: "gemini-3.1-flash-image-preview", options: { thinkingConfig: { thinkingLevel: "minimal" } } },
|
||||
{ id: "gemini-3-pro-image-preview", options: { thinkingConfig: { thinkingLevel: "high" } } },
|
||||
{ id: "gemini-2.5-pro", options: { thinkingConfig: { thinkingBudget: 128 } } },
|
||||
{ id: "gemini-2.5-flash", options: { thinkingConfig: { thinkingBudget: 0 } } },
|
||||
{ id: "gemini-3-pro-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } } },
|
||||
{ id: "gemini-3-flash-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" } } },
|
||||
{
|
||||
id: "gemini-3.1-flash-image-preview",
|
||||
options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" } },
|
||||
},
|
||||
{ id: "gemini-3-pro-image-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } } },
|
||||
{ id: "gemini-2.5-pro", options: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
|
||||
{ id: "gemini-2.5-flash", options: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
|
||||
]) {
|
||||
test(`${testCase.id} returns supported small thinking options`, () => {
|
||||
expect(ProviderTransform.smallOptions(createGoogleModel(testCase.id))).toEqual(testCase.options)
|
||||
})
|
||||
}
|
||||
|
||||
test("uses the first configured variant when available", () => {
|
||||
expect(
|
||||
ProviderTransform.smallOptions({
|
||||
...createGoogleModel("gemini-2.5-pro"),
|
||||
variants: {
|
||||
high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||
max: { thinkingConfig: { includeThoughts: true, thinkingBudget: 32768 } },
|
||||
},
|
||||
}),
|
||||
).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } })
|
||||
})
|
||||
|
||||
test("does not synthesize thinking options when variants are empty", () => {
|
||||
expect(ProviderTransform.smallOptions({ ...createGoogleModel("gemini-2.5-pro"), variants: {} })).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.providerOptions - ai-gateway-provider", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.14.48",
|
||||
"version": "1.14.49",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user