Compare commits

...

26 Commits

Author SHA1 Message Date
Kit Langton 87786c5e88 feat(voice): add session archive control 2026-07-28 11:09:14 -04:00
Brendan Allan 62c47f467f chore(client): regenerate project API 2026-07-23 12:06:18 +08:00
Brendan Allan 639a5fe5b1 test(project): update session project mock 2026-07-23 12:06:02 +08:00
Brendan Allan 9afd059fa1 feat(project): support project metadata updates 2026-07-23 12:05:51 +08:00
Brendan Allan 6bbf50b940 chore(client): regenerate session API 2026-07-23 12:05:43 +08:00
Brendan Allan ba2dc65d6b fix(session): keep archival project mock valid 2026-07-23 12:05:41 +08:00
Brendan Allan 08e616bcad feat(session): add durable session archival 2026-07-23 12:05:31 +08:00
Brendan Allan a6761d4fe0 chore(client): regenerate VCS API 2026-07-23 12:05:24 +08:00
Brendan Allan 305466e5ef feat(vcs): expose repository branch metadata 2026-07-23 12:05:12 +08:00
Brendan Allan 704e9ff3fb chore(client): regenerate PTY API 2026-07-23 12:05:05 +08:00
Brendan Allan dbe7b937ef feat(pty): expose shells and connect tokens 2026-07-23 12:04:43 +08:00
Brendan Allan 9de001d92f chore(client): regenerate command API 2026-07-23 12:04:37 +08:00
Brendan Allan 3cd894d40a feat(command): identify command sources 2026-07-23 12:04:35 +08:00
Brendan Allan 0e705e2a05 chore(client): regenerate path API 2026-07-23 12:04:25 +08:00
Brendan Allan 421d649019 fix(protocol): name path client group 2026-07-23 12:04:06 +08:00
Brendan Allan a7855aa169 feat(server): expose location paths 2026-07-23 12:03:46 +08:00
opencode-agent[bot] 6e8aefcfa0 fix(ai): normalize Bedrock cache usage (#38427)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-07-22 21:42:53 -05:00
James Long f1f0f47ee2 fix(core): migrate named agent colors (#38414) 2026-07-22 22:38:13 -04:00
Kit Langton 203b9f59b7 fix(core): load dynamic models for generation (#38401) 2026-07-22 22:26:08 -04:00
Aiden Cline 48bcbd09ef fix(ai): handle incomplete responses without reasons (#38374) 2026-07-22 20:49:08 -05:00
James Long d86f732df3 refactor(tui): generate syntax from V2 theme (#38397) 2026-07-22 21:33:25 -04:00
Dax Raad a817fe5e6c fix(schema): loosen agent color response 2026-07-22 19:22:01 -04:00
James Long 381f6c47b4 docs(tui): add generated V2 theme reference (#38396) 2026-07-22 18:30:31 -04:00
James Long b6e14b5a74 refactor(tui): finish V2 theme migration (#38383) 2026-07-22 17:56:22 -04:00
opencode-agent[bot] 36979c9641 test(core): consolidate provider factory coverage (#38390)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-07-22 16:50:33 -05:00
opencode-agent[bot] 4e067a2014 test(core): remove duplicate patch integration tests (#38389)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-07-22 16:19:06 -05:00
141 changed files with 7194 additions and 2313 deletions
+5
View File
@@ -97,6 +97,11 @@ jobs:
working-directory: packages/client
run: bun run check:generated
- name: Check generated documentation
if: runner.os == 'Linux'
working-directory: packages/docs
run: bun run check:generated
e2e:
name: e2e (${{ matrix.settings.name }})
if: github.ref_name != 'v2' && github.head_ref != 'v2'
+28
View File
@@ -470,7 +470,9 @@
"packages/docs": {
"name": "@opencode-ai/docs",
"devDependencies": {
"effect": "catalog:",
"mint": "4.2.666",
"prettier": "3.6.2",
},
},
"packages/effect-drizzle-sqlite": {
@@ -1005,6 +1007,24 @@
"@typescript/native-preview": "catalog:",
},
},
"packages/voice": {
"name": "@opencode-ai/voice",
"version": "0.0.0",
"dependencies": {
"@opencode-ai/client": "workspace:*",
"@opentui/core": "catalog:",
"@opentui/solid": "catalog:",
"effect": "catalog:",
"opentui-spinner": "catalog:",
"solid-js": "1.9.12",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
},
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.18.4",
@@ -2137,6 +2157,8 @@
"@opencode-ai/util": ["@opencode-ai/util@workspace:packages/util"],
"@opencode-ai/voice": ["@opencode-ai/voice@workspace:packages/voice"],
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
"@opencode-ai/www": ["@opencode-ai/www@workspace:packages/www"],
@@ -6605,6 +6627,8 @@
"@opencode-ai/updates/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="],
"@opencode-ai/voice/solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="],
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
"@opencode-ai/www/@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.44.0", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "4.20260708.1", "unenv": "2.0.0-rc.24", "wrangler": "4.110.0", "ws": "8.21.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" }, "bin": { "cf-vite": "bin/cf-vite" } }, "sha512-8wGGunqRcs34o4GRq0Rurp7GZg30xtLJeRGUU81a49r9zQRjlp3xIlsWr3nFlSCso4eE3cjZfiKC/2y116M4TQ=="],
@@ -7885,6 +7909,10 @@
"@opencode-ai/updates/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
"@opencode-ai/voice/solid-js/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="],
"@opencode-ai/voice/solid-js/seroval-plugins": ["seroval-plugins@1.5.5", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-+BDhqYM6CEn3x09v44dpa9p6974FuUB2dxk+Ctn04k0cO1Zt6QODTXfmEZK0eBaTe/fJBvP4NMGuNJ+R8T+QMg=="],
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
"@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
+10 -9
View File
@@ -436,21 +436,22 @@ const mapFinishReason = (reason: string): FinishReason => {
return "unknown"
}
// AWS Bedrock Converse reports `inputTokens` (inclusive total) with
// `cacheReadInputTokens` and `cacheWriteInputTokens` as subsets. Pass
// the total through and derive the non-cached breakdown. Bedrock does
// not break reasoning out of `outputTokens` for any current model.
// AWS reports inputTokens separately from cache reads and writes.
// Bedrock does not break reasoning out of outputTokens for current models.
const mapUsage = (usage: BedrockUsageSchema | undefined): Usage | undefined => {
if (!usage) return undefined
const cacheTotal = (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0)
const nonCached = ProviderShared.subtractTokens(usage.inputTokens, cacheTotal)
const inputTokens = ProviderShared.sumTokens(
usage.inputTokens,
usage.cacheReadInputTokens,
usage.cacheWriteInputTokens,
)
return new Usage({
inputTokens: usage.inputTokens,
inputTokens,
outputTokens: usage.outputTokens,
nonCachedInputTokens: nonCached,
nonCachedInputTokens: usage.inputTokens,
cacheReadInputTokens: usage.cacheReadInputTokens,
cacheWriteInputTokens: usage.cacheWriteInputTokens,
totalTokens: ProviderShared.totalTokens(usage.inputTokens, usage.outputTokens, usage.totalTokens),
totalTokens: ProviderShared.totalTokens(inputTokens, usage.outputTokens, usage.totalTokens),
providerMetadata: { bedrock: usage },
})
}
@@ -253,7 +253,7 @@ const OpenAIResponsesEvent = Schema.Struct({
Schema.Struct({
id: Schema.optional(Schema.String),
service_tier: optionalNull(Schema.String),
incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })),
incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })),
usage: optionalNull(OpenAIResponsesUsage),
error: optionalNull(OpenAIResponsesErrorPayload),
}),
@@ -602,7 +602,8 @@ const mapUsage = (usage: OpenAIResponsesUsage | null | undefined) => {
const mapFinishReason = (event: OpenAIResponsesEvent, hasFunctionCall: boolean): FinishReason => {
const reason = event.response?.incomplete_details?.reason
if (reason === undefined || reason === null) return hasFunctionCall ? "tool-calls" : "stop"
if (reason === undefined || reason === null)
return hasFunctionCall ? "tool-calls" : event.type === "response.incomplete" ? "unknown" : "stop"
if (reason === "max_output_tokens") return "length"
if (reason === "content_filter") return "content-filter"
return hasFunctionCall ? "tool-calls" : "unknown"
+4 -3
View File
@@ -34,11 +34,12 @@ import { ProviderFailureClassification } from "./errors"
*
* **Semantics by provider**:
*
* - OpenAI Chat / Responses / Gemini / Bedrock: provider reports inclusive
* - OpenAI Chat / Responses / Gemini: provider reports inclusive
* `inputTokens` and an inclusive `outputTokens`; mapper subtracts to
* derive the breakdown.
* - Anthropic: provider reports the breakdown natively (`input_tokens` is
* non-cached only); mapper sums to derive the inclusive `inputTokens`.
* - Anthropic and Bedrock report the input breakdown natively: Anthropic's
* `input_tokens` and Bedrock's `inputTokens` are non-cached only. Their
* mappers sum the breakdown to derive the inclusive `inputTokens`.
* Anthropic does *not* break extended-thinking out of `output_tokens`, so
* `reasoningTokens` is `undefined` and `outputTokens` carries the
* combined total — a documented limitation of the Anthropic API.
File diff suppressed because one or more lines are too long
@@ -13,12 +13,8 @@ const RECORDING_REGION = process.env.BEDROCK_RECORDING_REGION ?? "us-east-1"
// call wouldn't deterministically prove cache mapping works. Override with
// BEDROCK_CACHE_MODEL_ID if your account has access elsewhere.
const model = AmazonBedrock.configure({
credentials: {
region: RECORDING_REGION,
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "fixture",
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "fixture",
sessionToken: process.env.AWS_SESSION_TOKEN,
},
apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture",
region: RECORDING_REGION,
}).model(process.env.BEDROCK_CACHE_MODEL_ID ?? "us.anthropic.claude-haiku-4-5-20251001-v1:0")
const cacheRequest = LLM.request({
@@ -36,7 +32,7 @@ const recorded = recordedTests({
prefix: "bedrock-converse-cache",
provider: "amazon-bedrock",
protocol: "bedrock-converse",
requires: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
requires: ["AWS_BEARER_TOKEN_BEDROCK"],
// Two identical requests in one cassette — replay walks the cassette in
// recording order so the second call replays the cached-hit interaction.
})
@@ -45,10 +41,20 @@ describe("Bedrock Converse cache recorded", () => {
recorded.effect.with("writes then reads cachePoint on identical second call", { tags: ["cache"] }, () =>
Effect.gen(function* () {
const first = yield* LLMClient.generate(cacheRequest)
expect(first.usage?.cacheReadInputTokens ?? 0).toBeGreaterThanOrEqual(0)
expect(first.usage?.cacheWriteInputTokens ?? 0).toBeGreaterThan(0)
expect(first.usage?.inputTokens).toBe(
(first.usage?.nonCachedInputTokens ?? 0) +
(first.usage?.cacheReadInputTokens ?? 0) +
(first.usage?.cacheWriteInputTokens ?? 0),
)
const second = yield* LLMClient.generate(cacheRequest)
expect(second.usage?.cacheReadInputTokens ?? 0).toBeGreaterThan(0)
expect(second.usage?.inputTokens).toBe(
(second.usage?.nonCachedInputTokens ?? 0) +
(second.usage?.cacheReadInputTokens ?? 0) +
(second.usage?.cacheWriteInputTokens ?? 0),
)
}),
)
})
@@ -269,6 +269,39 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("adds cache reads and writes to Bedrock input usage", () =>
Effect.gen(function* () {
const body = eventStreamBody(
["messageStart", { role: "assistant" }],
["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hello" } }],
["contentBlockStop", { contentBlockIndex: 0 }],
["messageStop", { stopReason: "end_turn" }],
[
"metadata",
{
usage: {
inputTokens: 5,
outputTokens: 2,
totalTokens: 12,
cacheReadInputTokens: 3,
cacheWriteInputTokens: 2,
},
},
],
)
const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)))
expect(response.usage).toMatchObject({
inputTokens: 10,
nonCachedInputTokens: 5,
cacheReadInputTokens: 3,
cacheWriteInputTokens: 2,
outputTokens: 2,
totalTokens: 12,
})
}),
)
it.effect("assembles streamed tool call input", () =>
Effect.gen(function* () {
const body = eventStreamBody(
@@ -870,6 +870,32 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("maps incomplete response reasons", () =>
Effect.gen(function* () {
const generate = (incompleteDetails: object) =>
LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents({
type: "response.incomplete",
response: { id: "resp_incomplete", incomplete_details: incompleteDetails },
}),
),
),
)
const length = yield* generate({ reason: "max_output_tokens" })
const contentFilter = yield* generate({ reason: "content_filter" })
const unknown = yield* generate({})
expect([length.finishReason, contentFilter.finishReason, unknown.finishReason]).toEqual([
"length",
"content-filter",
"unknown",
])
}),
)
// OpenAI's documented stream orders output text within one message item; no
// provider-valid same-kind overlap is evidenced, so done boundaries close it.
it.effect("closes sequential output messages before starting the next", () =>
+210 -161
View File
@@ -124,185 +124,190 @@ export type Endpoint5_8Input = {
export type Endpoint5_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>>
export type SessionRenameOperation<E = never> = (input: Endpoint5_8Input) => Effect.Effect<Endpoint5_8Output, E>
type Endpoint5_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
export type Endpoint5_9Input = {
readonly sessionID: Endpoint5_9Request["params"]["sessionID"]
readonly directory: Endpoint5_9Request["payload"]["directory"]
readonly workspaceID?: Endpoint5_9Request["payload"]["workspaceID"]
}
export type Endpoint5_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.move"]>>
export type SessionMoveOperation<E = never> = (input: Endpoint5_9Input) => Effect.Effect<Endpoint5_9Output, E>
type Endpoint5_9Request = Parameters<RawClient["server.session"]["session.archive"]>[0]
export type Endpoint5_9Input = { readonly sessionID: Endpoint5_9Request["params"]["sessionID"] }
export type Endpoint5_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.archive"]>>
export type SessionArchiveOperation<E = never> = (input: Endpoint5_9Input) => Effect.Effect<Endpoint5_9Output, E>
type Endpoint5_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint5_10Request = Parameters<RawClient["server.session"]["session.move"]>[0]
export type Endpoint5_10Input = {
readonly sessionID: Endpoint5_10Request["params"]["sessionID"]
readonly id?: Endpoint5_10Request["payload"]["id"]
readonly text: Endpoint5_10Request["payload"]["text"]
readonly files?: Endpoint5_10Request["payload"]["files"]
readonly agents?: Endpoint5_10Request["payload"]["agents"]
readonly metadata?: Endpoint5_10Request["payload"]["metadata"]
readonly delivery?: Endpoint5_10Request["payload"]["delivery"]
readonly resume?: Endpoint5_10Request["payload"]["resume"]
readonly directory: Endpoint5_10Request["payload"]["directory"]
readonly workspaceID?: Endpoint5_10Request["payload"]["workspaceID"]
}
export type Endpoint5_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
export type SessionPromptOperation<E = never> = (input: Endpoint5_10Input) => Effect.Effect<Endpoint5_10Output, E>
export type Endpoint5_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.move"]>>
export type SessionMoveOperation<E = never> = (input: Endpoint5_10Input) => Effect.Effect<Endpoint5_10Output, E>
type Endpoint5_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint5_11Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
export type Endpoint5_11Input = {
readonly sessionID: Endpoint5_11Request["params"]["sessionID"]
readonly id?: Endpoint5_11Request["payload"]["id"]
readonly command: Endpoint5_11Request["payload"]["command"]
readonly arguments?: Endpoint5_11Request["payload"]["arguments"]
readonly agent?: Endpoint5_11Request["payload"]["agent"]
readonly model?: Endpoint5_11Request["payload"]["model"]
readonly text: Endpoint5_11Request["payload"]["text"]
readonly files?: Endpoint5_11Request["payload"]["files"]
readonly agents?: Endpoint5_11Request["payload"]["agents"]
readonly metadata?: Endpoint5_11Request["payload"]["metadata"]
readonly delivery?: Endpoint5_11Request["payload"]["delivery"]
readonly resume?: Endpoint5_11Request["payload"]["resume"]
}
export type Endpoint5_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
export type SessionCommandOperation<E = never> = (input: Endpoint5_11Input) => Effect.Effect<Endpoint5_11Output, E>
export type Endpoint5_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
export type SessionPromptOperation<E = never> = (input: Endpoint5_11Input) => Effect.Effect<Endpoint5_11Output, E>
type Endpoint5_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint5_12Request = Parameters<RawClient["server.session"]["session.command"]>[0]
export type Endpoint5_12Input = {
readonly sessionID: Endpoint5_12Request["params"]["sessionID"]
readonly id?: Endpoint5_12Request["payload"]["id"]
readonly skill: Endpoint5_12Request["payload"]["skill"]
readonly command: Endpoint5_12Request["payload"]["command"]
readonly arguments?: Endpoint5_12Request["payload"]["arguments"]
readonly agent?: Endpoint5_12Request["payload"]["agent"]
readonly model?: Endpoint5_12Request["payload"]["model"]
readonly files?: Endpoint5_12Request["payload"]["files"]
readonly agents?: Endpoint5_12Request["payload"]["agents"]
readonly delivery?: Endpoint5_12Request["payload"]["delivery"]
readonly resume?: Endpoint5_12Request["payload"]["resume"]
}
export type Endpoint5_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
export type SessionSkillOperation<E = never> = (input: Endpoint5_12Input) => Effect.Effect<Endpoint5_12Output, E>
export type Endpoint5_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
export type SessionCommandOperation<E = never> = (input: Endpoint5_12Input) => Effect.Effect<Endpoint5_12Output, E>
type Endpoint5_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint5_13Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
export type Endpoint5_13Input = {
readonly sessionID: Endpoint5_13Request["params"]["sessionID"]
readonly id?: Endpoint5_13Request["payload"]["id"]
readonly text: Endpoint5_13Request["payload"]["text"]
readonly description?: Endpoint5_13Request["payload"]["description"]
readonly metadata?: Endpoint5_13Request["payload"]["metadata"]
readonly delivery?: Endpoint5_13Request["payload"]["delivery"]
readonly skill: Endpoint5_13Request["payload"]["skill"]
readonly resume?: Endpoint5_13Request["payload"]["resume"]
}
export type Endpoint5_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>["data"]
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
export type Endpoint5_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
export type SessionSkillOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
type Endpoint5_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint5_14Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
export type Endpoint5_14Input = {
readonly sessionID: Endpoint5_14Request["params"]["sessionID"]
readonly id?: Endpoint5_14Request["payload"]["id"]
readonly command: Endpoint5_14Request["payload"]["command"]
readonly text: Endpoint5_14Request["payload"]["text"]
readonly description?: Endpoint5_14Request["payload"]["description"]
readonly metadata?: Endpoint5_14Request["payload"]["metadata"]
readonly delivery?: Endpoint5_14Request["payload"]["delivery"]
readonly resume?: Endpoint5_14Request["payload"]["resume"]
}
export type Endpoint5_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
export type SessionShellOperation<E = never> = (input: Endpoint5_14Input) => Effect.Effect<Endpoint5_14Output, E>
export type Endpoint5_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>["data"]
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_14Input) => Effect.Effect<Endpoint5_14Output, E>
type Endpoint5_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint5_15Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
export type Endpoint5_15Input = {
readonly sessionID: Endpoint5_15Request["params"]["sessionID"]
readonly id?: Endpoint5_15Request["payload"]["id"]
readonly command: Endpoint5_15Request["payload"]["command"]
}
export type Endpoint5_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
export type SessionCompactOperation<E = never> = (input: Endpoint5_15Input) => Effect.Effect<Endpoint5_15Output, E>
export type Endpoint5_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
export type SessionShellOperation<E = never> = (input: Endpoint5_15Input) => Effect.Effect<Endpoint5_15Output, E>
type Endpoint5_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
export type Endpoint5_16Input = { readonly sessionID: Endpoint5_16Request["params"]["sessionID"] }
export type Endpoint5_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
export type SessionWaitOperation<E = never> = (input: Endpoint5_16Input) => Effect.Effect<Endpoint5_16Output, E>
type Endpoint5_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
export type Endpoint5_17Input = {
readonly sessionID: Endpoint5_17Request["params"]["sessionID"]
readonly messageID: Endpoint5_17Request["payload"]["messageID"]
readonly files?: Endpoint5_17Request["payload"]["files"]
type Endpoint5_16Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
export type Endpoint5_16Input = {
readonly sessionID: Endpoint5_16Request["params"]["sessionID"]
readonly id?: Endpoint5_16Request["payload"]["id"]
}
export type Endpoint5_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
export type SessionRevertStageOperation<E = never> = (input: Endpoint5_17Input) => Effect.Effect<Endpoint5_17Output, E>
export type Endpoint5_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
export type SessionCompactOperation<E = never> = (input: Endpoint5_16Input) => Effect.Effect<Endpoint5_16Output, E>
type Endpoint5_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
export type Endpoint5_18Input = { readonly sessionID: Endpoint5_18Request["params"]["sessionID"] }
export type Endpoint5_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
export type SessionRevertClearOperation<E = never> = (input: Endpoint5_18Input) => Effect.Effect<Endpoint5_18Output, E>
type Endpoint5_17Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
export type Endpoint5_17Input = { readonly sessionID: Endpoint5_17Request["params"]["sessionID"] }
export type Endpoint5_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
export type SessionWaitOperation<E = never> = (input: Endpoint5_17Input) => Effect.Effect<Endpoint5_17Output, E>
type Endpoint5_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint5_18Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
export type Endpoint5_18Input = {
readonly sessionID: Endpoint5_18Request["params"]["sessionID"]
readonly messageID: Endpoint5_18Request["payload"]["messageID"]
readonly files?: Endpoint5_18Request["payload"]["files"]
}
export type Endpoint5_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
export type SessionRevertStageOperation<E = never> = (input: Endpoint5_18Input) => Effect.Effect<Endpoint5_18Output, E>
type Endpoint5_19Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
export type Endpoint5_19Input = { readonly sessionID: Endpoint5_19Request["params"]["sessionID"] }
export type Endpoint5_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
export type SessionRevertCommitOperation<E = never> = (input: Endpoint5_19Input) => Effect.Effect<Endpoint5_19Output, E>
export type Endpoint5_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
export type SessionRevertClearOperation<E = never> = (input: Endpoint5_19Input) => Effect.Effect<Endpoint5_19Output, E>
type Endpoint5_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint5_20Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
export type Endpoint5_20Input = { readonly sessionID: Endpoint5_20Request["params"]["sessionID"] }
export type Endpoint5_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
export type SessionContextOperation<E = never> = (input: Endpoint5_20Input) => Effect.Effect<Endpoint5_20Output, E>
export type Endpoint5_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
export type SessionRevertCommitOperation<E = never> = (input: Endpoint5_20Input) => Effect.Effect<Endpoint5_20Output, E>
type Endpoint5_21Request = Parameters<RawClient["server.session"]["session.pending.list"]>[0]
type Endpoint5_21Request = Parameters<RawClient["server.session"]["session.context"]>[0]
export type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] }
export type Endpoint5_21Output = EffectValue<ReturnType<RawClient["server.session"]["session.pending.list"]>>["data"]
export type SessionPendingListOperation<E = never> = (input: Endpoint5_21Input) => Effect.Effect<Endpoint5_21Output, E>
export type Endpoint5_21Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
export type SessionContextOperation<E = never> = (input: Endpoint5_21Input) => Effect.Effect<Endpoint5_21Output, E>
type Endpoint5_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
type Endpoint5_22Request = Parameters<RawClient["server.session"]["session.pending.list"]>[0]
export type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] }
export type Endpoint5_22Output = EffectValue<
export type Endpoint5_22Output = EffectValue<ReturnType<RawClient["server.session"]["session.pending.list"]>>["data"]
export type SessionPendingListOperation<E = never> = (input: Endpoint5_22Input) => Effect.Effect<Endpoint5_22Output, E>
type Endpoint5_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
export type Endpoint5_23Input = { readonly sessionID: Endpoint5_23Request["params"]["sessionID"] }
export type Endpoint5_23Output = EffectValue<
ReturnType<RawClient["server.session"]["session.instructions.entry.list"]>
>["data"]
export type SessionInstructionsEntryListOperation<E = never> = (
input: Endpoint5_22Input,
) => Effect.Effect<Endpoint5_22Output, E>
type Endpoint5_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
export type Endpoint5_23Input = {
readonly sessionID: Endpoint5_23Request["params"]["sessionID"]
readonly key: Endpoint5_23Request["params"]["key"]
readonly value: Endpoint5_23Request["payload"]["value"]
}
export type Endpoint5_23Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
export type SessionInstructionsEntryPutOperation<E = never> = (
input: Endpoint5_23Input,
) => Effect.Effect<Endpoint5_23Output, E>
type Endpoint5_24Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
type Endpoint5_24Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
export type Endpoint5_24Input = {
readonly sessionID: Endpoint5_24Request["params"]["sessionID"]
readonly key: Endpoint5_24Request["params"]["key"]
readonly value: Endpoint5_24Request["payload"]["value"]
}
export type Endpoint5_24Output = EffectValue<
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
>
export type SessionInstructionsEntryRemoveOperation<E = never> = (
export type Endpoint5_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
export type SessionInstructionsEntryPutOperation<E = never> = (
input: Endpoint5_24Input,
) => Effect.Effect<Endpoint5_24Output, E>
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.generate"]>[0]
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
export type Endpoint5_25Input = {
readonly sessionID: Endpoint5_25Request["params"]["sessionID"]
readonly prompt: Endpoint5_25Request["payload"]["prompt"]
readonly key: Endpoint5_25Request["params"]["key"]
}
export type Endpoint5_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.generate"]>>["data"]
export type SessionGenerateOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
export type Endpoint5_25Output = EffectValue<
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
>
export type SessionInstructionsEntryRemoveOperation<E = never> = (
input: Endpoint5_25Input,
) => Effect.Effect<Endpoint5_25Output, E>
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.generate"]>[0]
export type Endpoint5_26Input = {
readonly sessionID: Endpoint5_26Request["params"]["sessionID"]
readonly after?: Endpoint5_26Request["query"]["after"]
readonly follow?: Endpoint5_26Request["query"]["follow"]
readonly prompt: Endpoint5_26Request["payload"]["prompt"]
}
export type Endpoint5_26Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
export type SessionLogOperation<E = never> = (input: Endpoint5_26Input) => Stream.Stream<Endpoint5_26Output, E>
export type Endpoint5_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.generate"]>>["data"]
export type SessionGenerateOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
type Endpoint5_27Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
export type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] }
export type Endpoint5_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
export type SessionInterruptOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E>
type Endpoint5_27Request = Parameters<RawClient["server.session"]["session.log"]>[0]
export type Endpoint5_27Input = {
readonly sessionID: Endpoint5_27Request["params"]["sessionID"]
readonly after?: Endpoint5_27Request["query"]["after"]
readonly follow?: Endpoint5_27Request["query"]["follow"]
}
export type Endpoint5_27Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
export type SessionLogOperation<E = never> = (input: Endpoint5_27Input) => Stream.Stream<Endpoint5_27Output, E>
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
export type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] }
export type Endpoint5_28Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_28Input) => Effect.Effect<Endpoint5_28Output, E>
export type Endpoint5_28Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
export type SessionInterruptOperation<E = never> = (input: Endpoint5_28Input) => Effect.Effect<Endpoint5_28Output, E>
type Endpoint5_29Request = Parameters<RawClient["server.session"]["session.message"]>[0]
export type Endpoint5_29Input = {
readonly sessionID: Endpoint5_29Request["params"]["sessionID"]
readonly messageID: Endpoint5_29Request["params"]["messageID"]
type Endpoint5_29Request = Parameters<RawClient["server.session"]["session.background"]>[0]
export type Endpoint5_29Input = { readonly sessionID: Endpoint5_29Request["params"]["sessionID"] }
export type Endpoint5_29Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
type Endpoint5_30Request = Parameters<RawClient["server.session"]["session.message"]>[0]
export type Endpoint5_30Input = {
readonly sessionID: Endpoint5_30Request["params"]["sessionID"]
readonly messageID: Endpoint5_30Request["params"]["messageID"]
}
export type Endpoint5_29Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
export type SessionMessageOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
export type Endpoint5_30Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
export type SessionMessageOperation<E = never> = (input: Endpoint5_30Input) => Effect.Effect<Endpoint5_30Output, E>
export interface SessionApi<E = never> {
readonly list: SessionListOperation<E>
@@ -314,6 +319,7 @@ export interface SessionApi<E = never> {
readonly switchAgent: SessionSwitchAgentOperation<E>
readonly switchModel: SessionSwitchModelOperation<E>
readonly rename: SessionRenameOperation<E>
readonly archive: SessionArchiveOperation<E>
readonly move: SessionMoveOperation<E>
readonly prompt: SessionPromptOperation<E>
readonly command: SessionCommandOperation<E>
@@ -614,21 +620,32 @@ export interface CredentialApi<E = never> {
export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.project"]["project.list"]>>
export type ProjectListOperation<E = never> = () => Effect.Effect<Endpoint13_0Output, E>
type Endpoint13_1Request = Parameters<RawClient["server.project"]["project.current"]>[0]
export type Endpoint13_1Input = { readonly location?: Endpoint13_1Request["query"]["location"] }
export type Endpoint13_1Output = EffectValue<ReturnType<RawClient["server.project"]["project.current"]>>
export type ProjectCurrentOperation<E = never> = (input?: Endpoint13_1Input) => Effect.Effect<Endpoint13_1Output, E>
type Endpoint13_2Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
export type Endpoint13_2Input = {
readonly projectID: Endpoint13_2Request["params"]["projectID"]
readonly location?: Endpoint13_2Request["query"]["location"]
type Endpoint13_1Request = Parameters<RawClient["server.project"]["project.update"]>[0]
export type Endpoint13_1Input = {
readonly projectID: Endpoint13_1Request["params"]["projectID"]
readonly name?: Endpoint13_1Request["payload"]["name"]
readonly icon?: Endpoint13_1Request["payload"]["icon"]
readonly commands?: Endpoint13_1Request["payload"]["commands"]
}
export type Endpoint13_2Output = EffectValue<ReturnType<RawClient["server.project"]["project.directories"]>>
export type ProjectDirectoriesOperation<E = never> = (input: Endpoint13_2Input) => Effect.Effect<Endpoint13_2Output, E>
export type Endpoint13_1Output = EffectValue<ReturnType<RawClient["server.project"]["project.update"]>>
export type ProjectUpdateOperation<E = never> = (input: Endpoint13_1Input) => Effect.Effect<Endpoint13_1Output, E>
type Endpoint13_2Request = Parameters<RawClient["server.project"]["project.current"]>[0]
export type Endpoint13_2Input = { readonly location?: Endpoint13_2Request["query"]["location"] }
export type Endpoint13_2Output = EffectValue<ReturnType<RawClient["server.project"]["project.current"]>>
export type ProjectCurrentOperation<E = never> = (input?: Endpoint13_2Input) => Effect.Effect<Endpoint13_2Output, E>
type Endpoint13_3Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
export type Endpoint13_3Input = {
readonly projectID: Endpoint13_3Request["params"]["projectID"]
readonly location?: Endpoint13_3Request["query"]["location"]
}
export type Endpoint13_3Output = EffectValue<ReturnType<RawClient["server.project"]["project.directories"]>>
export type ProjectDirectoriesOperation<E = never> = (input: Endpoint13_3Input) => Effect.Effect<Endpoint13_3Output, E>
export interface ProjectApi<E = never> {
readonly list: ProjectListOperation<E>
readonly update: ProjectUpdateOperation<E>
readonly current: ProjectCurrentOperation<E>
readonly directories: ProjectDirectoriesOperation<E>
}
@@ -820,55 +837,71 @@ export interface EventApi<E = never> {
readonly subscribe: EventSubscribeOperation<E>
}
type Endpoint20_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
type Endpoint20_0Request = Parameters<RawClient["server.pty"]["pty.shells"]>[0]
export type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
export type Endpoint20_0Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.list"]>>
export type PtyListOperation<E = never> = (input?: Endpoint20_0Input) => Effect.Effect<Endpoint20_0Output, E>
export type Endpoint20_0Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.shells"]>>
export type PtyShellsOperation<E = never> = (input?: Endpoint20_0Input) => Effect.Effect<Endpoint20_0Output, E>
type Endpoint20_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
export type Endpoint20_1Input = {
readonly location?: Endpoint20_1Request["query"]["location"]
readonly command?: Endpoint20_1Request["payload"]["command"]
readonly args?: Endpoint20_1Request["payload"]["args"]
readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
readonly title?: Endpoint20_1Request["payload"]["title"]
readonly env?: Endpoint20_1Request["payload"]["env"]
}
export type Endpoint20_1Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.create"]>>
export type PtyCreateOperation<E = never> = (input?: Endpoint20_1Input) => Effect.Effect<Endpoint20_1Output, E>
type Endpoint20_1Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
export type Endpoint20_1Input = { readonly location?: Endpoint20_1Request["query"]["location"] }
export type Endpoint20_1Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.list"]>>
export type PtyListOperation<E = never> = (input?: Endpoint20_1Input) => Effect.Effect<Endpoint20_1Output, E>
type Endpoint20_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
type Endpoint20_2Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
export type Endpoint20_2Input = {
readonly ptyID: Endpoint20_2Request["params"]["ptyID"]
readonly location?: Endpoint20_2Request["query"]["location"]
readonly command?: Endpoint20_2Request["payload"]["command"]
readonly args?: Endpoint20_2Request["payload"]["args"]
readonly cwd?: Endpoint20_2Request["payload"]["cwd"]
readonly title?: Endpoint20_2Request["payload"]["title"]
readonly env?: Endpoint20_2Request["payload"]["env"]
}
export type Endpoint20_2Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.get"]>>
export type PtyGetOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
export type Endpoint20_2Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.create"]>>
export type PtyCreateOperation<E = never> = (input?: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
type Endpoint20_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
type Endpoint20_3Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
export type Endpoint20_3Input = {
readonly ptyID: Endpoint20_3Request["params"]["ptyID"]
readonly location?: Endpoint20_3Request["query"]["location"]
readonly title?: Endpoint20_3Request["payload"]["title"]
readonly size?: Endpoint20_3Request["payload"]["size"]
}
export type Endpoint20_3Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.update"]>>
export type PtyUpdateOperation<E = never> = (input: Endpoint20_3Input) => Effect.Effect<Endpoint20_3Output, E>
export type Endpoint20_3Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.get"]>>
export type PtyGetOperation<E = never> = (input: Endpoint20_3Input) => Effect.Effect<Endpoint20_3Output, E>
type Endpoint20_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
type Endpoint20_4Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
export type Endpoint20_4Input = {
readonly ptyID: Endpoint20_4Request["params"]["ptyID"]
readonly location?: Endpoint20_4Request["query"]["location"]
readonly title?: Endpoint20_4Request["payload"]["title"]
readonly size?: Endpoint20_4Request["payload"]["size"]
}
export type Endpoint20_4Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.remove"]>>
export type PtyRemoveOperation<E = never> = (input: Endpoint20_4Input) => Effect.Effect<Endpoint20_4Output, E>
export type Endpoint20_4Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.update"]>>
export type PtyUpdateOperation<E = never> = (input: Endpoint20_4Input) => Effect.Effect<Endpoint20_4Output, E>
type Endpoint20_5Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
export type Endpoint20_5Input = {
readonly ptyID: Endpoint20_5Request["params"]["ptyID"]
readonly location?: Endpoint20_5Request["query"]["location"]
}
export type Endpoint20_5Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.remove"]>>
export type PtyRemoveOperation<E = never> = (input: Endpoint20_5Input) => Effect.Effect<Endpoint20_5Output, E>
type Endpoint20_6Request = Parameters<RawClient["server.pty"]["pty.connectToken"]>[0]
export type Endpoint20_6Input = {
readonly ptyID: Endpoint20_6Request["params"]["ptyID"]
readonly location?: Endpoint20_6Request["query"]["location"]
readonly "x-opencode-ticket"?: Endpoint20_6Request["headers"]["x-opencode-ticket"]
}
export type Endpoint20_6Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.connectToken"]>>
export type PtyConnectTokenOperation<E = never> = (input: Endpoint20_6Input) => Effect.Effect<Endpoint20_6Output, E>
export interface PtyApi<E = never> {
readonly shells: PtyShellsOperation<E>
readonly list: PtyListOperation<E>
readonly create: PtyCreateOperation<E>
readonly get: PtyGetOperation<E>
readonly update: PtyUpdateOperation<E>
readonly remove: PtyRemoveOperation<E>
readonly connectToken: PtyConnectTokenOperation<E>
}
type Endpoint21_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
@@ -1011,32 +1044,47 @@ export interface ProjectCopyApi<E = never> {
readonly refresh: ProjectCopyRefreshOperation<E>
}
type Endpoint25_0Request = Parameters<RawClient["server.vcs"]["vcs.status"]>[0]
type Endpoint25_0Request = Parameters<RawClient["server.vcs"]["vcs.get"]>[0]
export type Endpoint25_0Input = { readonly location?: Endpoint25_0Request["query"]["location"] }
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.vcs"]["vcs.status"]>>
export type VcsStatusOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E>
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.vcs"]["vcs.get"]>>
export type VcsGetOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E>
type Endpoint25_1Request = Parameters<RawClient["server.vcs"]["vcs.diff"]>[0]
export type Endpoint25_1Input = {
readonly location?: Endpoint25_1Request["query"]["location"]
readonly mode: Endpoint25_1Request["query"]["mode"]
readonly context?: Endpoint25_1Request["query"]["context"]
type Endpoint25_1Request = Parameters<RawClient["server.vcs"]["vcs.status"]>[0]
export type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
export type Endpoint25_1Output = EffectValue<ReturnType<RawClient["server.vcs"]["vcs.status"]>>
export type VcsStatusOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
type Endpoint25_2Request = Parameters<RawClient["server.vcs"]["vcs.diff"]>[0]
export type Endpoint25_2Input = {
readonly location?: Endpoint25_2Request["query"]["location"]
readonly mode: Endpoint25_2Request["query"]["mode"]
readonly context?: Endpoint25_2Request["query"]["context"]
}
export type Endpoint25_1Output = EffectValue<ReturnType<RawClient["server.vcs"]["vcs.diff"]>>
export type VcsDiffOperation<E = never> = (input: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
export type Endpoint25_2Output = EffectValue<ReturnType<RawClient["server.vcs"]["vcs.diff"]>>
export type VcsDiffOperation<E = never> = (input: Endpoint25_2Input) => Effect.Effect<Endpoint25_2Output, E>
export interface VcsApi<E = never> {
readonly get: VcsGetOperation<E>
readonly status: VcsStatusOperation<E>
readonly diff: VcsDiffOperation<E>
}
export type Endpoint26_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint26_0Output, E>
type Endpoint26_0Request = Parameters<RawClient["server.path"]["path.get"]>[0]
export type Endpoint26_0Input = { readonly location?: Endpoint26_0Request["query"]["location"] }
export type Endpoint26_0Output = EffectValue<ReturnType<RawClient["server.path"]["path.get"]>>
export type PathGetOperation<E = never> = (input?: Endpoint26_0Input) => Effect.Effect<Endpoint26_0Output, E>
type Endpoint26_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
export type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] }
export type Endpoint26_1Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location.evict"]>>
export type DebugLocationEvictOperation<E = never> = (input?: Endpoint26_1Input) => Effect.Effect<Endpoint26_1Output, E>
export interface PathApi<E = never> {
readonly get: PathGetOperation<E>
}
export type Endpoint27_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint27_0Output, E>
type Endpoint27_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
export type Endpoint27_1Input = { readonly location?: Endpoint27_1Request["query"]["location"] }
export type Endpoint27_1Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location.evict"]>>
export type DebugLocationEvictOperation<E = never> = (input?: Endpoint27_1Input) => Effect.Effect<Endpoint27_1Output, E>
export interface DebugApi<E = never> {
readonly location: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> }
@@ -1069,5 +1117,6 @@ export interface AppApi<E = never> {
readonly reference: ReferenceApi<E>
readonly projectCopy: ProjectCopyApi<E>
readonly vcs: VcsApi<E>
readonly path: PathApi<E>
readonly debug: DebugApi<E>
}
+245 -188
View File
@@ -151,30 +151,35 @@ const Endpoint5_8 = (raw: RawClient["server.session"]) => (input: Endpoint5_8Inp
Effect.mapError(mapClientError),
)
type Endpoint5_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
type Endpoint5_9Input = {
readonly sessionID: Endpoint5_9Request["params"]["sessionID"]
readonly directory: Endpoint5_9Request["payload"]["directory"]
readonly workspaceID?: Endpoint5_9Request["payload"]["workspaceID"]
}
type Endpoint5_9Request = Parameters<RawClient["server.session"]["session.archive"]>[0]
type Endpoint5_9Input = { readonly sessionID: Endpoint5_9Request["params"]["sessionID"] }
const Endpoint5_9 = (raw: RawClient["server.session"]) => (input: Endpoint5_9Input) =>
raw["session.archive"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint5_10Request = Parameters<RawClient["server.session"]["session.move"]>[0]
type Endpoint5_10Input = {
readonly sessionID: Endpoint5_10Request["params"]["sessionID"]
readonly directory: Endpoint5_10Request["payload"]["directory"]
readonly workspaceID?: Endpoint5_10Request["payload"]["workspaceID"]
}
const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) =>
raw["session.move"]({
params: { sessionID: input["sessionID"] },
payload: { directory: input["directory"], workspaceID: input["workspaceID"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint5_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint5_10Input = {
readonly sessionID: Endpoint5_10Request["params"]["sessionID"]
readonly id?: Endpoint5_10Request["payload"]["id"]
readonly text: Endpoint5_10Request["payload"]["text"]
readonly files?: Endpoint5_10Request["payload"]["files"]
readonly agents?: Endpoint5_10Request["payload"]["agents"]
readonly metadata?: Endpoint5_10Request["payload"]["metadata"]
readonly delivery?: Endpoint5_10Request["payload"]["delivery"]
readonly resume?: Endpoint5_10Request["payload"]["resume"]
type Endpoint5_11Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint5_11Input = {
readonly sessionID: Endpoint5_11Request["params"]["sessionID"]
readonly id?: Endpoint5_11Request["payload"]["id"]
readonly text: Endpoint5_11Request["payload"]["text"]
readonly files?: Endpoint5_11Request["payload"]["files"]
readonly agents?: Endpoint5_11Request["payload"]["agents"]
readonly metadata?: Endpoint5_11Request["payload"]["metadata"]
readonly delivery?: Endpoint5_11Request["payload"]["delivery"]
readonly resume?: Endpoint5_11Request["payload"]["resume"]
}
const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) =>
const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) =>
raw["session.prompt"]({
params: { sessionID: input["sessionID"] },
payload: {
@@ -191,20 +196,20 @@ const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10I
Effect.map((value) => value.data),
)
type Endpoint5_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint5_11Input = {
readonly sessionID: Endpoint5_11Request["params"]["sessionID"]
readonly id?: Endpoint5_11Request["payload"]["id"]
readonly command: Endpoint5_11Request["payload"]["command"]
readonly arguments?: Endpoint5_11Request["payload"]["arguments"]
readonly agent?: Endpoint5_11Request["payload"]["agent"]
readonly model?: Endpoint5_11Request["payload"]["model"]
readonly files?: Endpoint5_11Request["payload"]["files"]
readonly agents?: Endpoint5_11Request["payload"]["agents"]
readonly delivery?: Endpoint5_11Request["payload"]["delivery"]
readonly resume?: Endpoint5_11Request["payload"]["resume"]
type Endpoint5_12Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint5_12Input = {
readonly sessionID: Endpoint5_12Request["params"]["sessionID"]
readonly id?: Endpoint5_12Request["payload"]["id"]
readonly command: Endpoint5_12Request["payload"]["command"]
readonly arguments?: Endpoint5_12Request["payload"]["arguments"]
readonly agent?: Endpoint5_12Request["payload"]["agent"]
readonly model?: Endpoint5_12Request["payload"]["model"]
readonly files?: Endpoint5_12Request["payload"]["files"]
readonly agents?: Endpoint5_12Request["payload"]["agents"]
readonly delivery?: Endpoint5_12Request["payload"]["delivery"]
readonly resume?: Endpoint5_12Request["payload"]["resume"]
}
const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) =>
const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) =>
raw["session.command"]({
params: { sessionID: input["sessionID"] },
payload: {
@@ -223,30 +228,30 @@ const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11I
Effect.map((value) => value.data),
)
type Endpoint5_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint5_12Input = {
readonly sessionID: Endpoint5_12Request["params"]["sessionID"]
readonly id?: Endpoint5_12Request["payload"]["id"]
readonly skill: Endpoint5_12Request["payload"]["skill"]
readonly resume?: Endpoint5_12Request["payload"]["resume"]
type Endpoint5_13Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint5_13Input = {
readonly sessionID: Endpoint5_13Request["params"]["sessionID"]
readonly id?: Endpoint5_13Request["payload"]["id"]
readonly skill: Endpoint5_13Request["payload"]["skill"]
readonly resume?: Endpoint5_13Request["payload"]["resume"]
}
const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) =>
const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) =>
raw["session.skill"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint5_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint5_13Input = {
readonly sessionID: Endpoint5_13Request["params"]["sessionID"]
readonly id?: Endpoint5_13Request["payload"]["id"]
readonly text: Endpoint5_13Request["payload"]["text"]
readonly description?: Endpoint5_13Request["payload"]["description"]
readonly metadata?: Endpoint5_13Request["payload"]["metadata"]
readonly delivery?: Endpoint5_13Request["payload"]["delivery"]
readonly resume?: Endpoint5_13Request["payload"]["resume"]
type Endpoint5_14Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint5_14Input = {
readonly sessionID: Endpoint5_14Request["params"]["sessionID"]
readonly id?: Endpoint5_14Request["payload"]["id"]
readonly text: Endpoint5_14Request["payload"]["text"]
readonly description?: Endpoint5_14Request["payload"]["description"]
readonly metadata?: Endpoint5_14Request["payload"]["metadata"]
readonly delivery?: Endpoint5_14Request["payload"]["delivery"]
readonly resume?: Endpoint5_14Request["payload"]["resume"]
}
const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) =>
const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) =>
raw["session.synthetic"]({
params: { sessionID: input["sessionID"] },
payload: {
@@ -262,41 +267,41 @@ const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13I
Effect.map((value) => value.data),
)
type Endpoint5_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint5_14Input = {
readonly sessionID: Endpoint5_14Request["params"]["sessionID"]
readonly id?: Endpoint5_14Request["payload"]["id"]
readonly command: Endpoint5_14Request["payload"]["command"]
type Endpoint5_15Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint5_15Input = {
readonly sessionID: Endpoint5_15Request["params"]["sessionID"]
readonly id?: Endpoint5_15Request["payload"]["id"]
readonly command: Endpoint5_15Request["payload"]["command"]
}
const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) =>
const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) =>
raw["session.shell"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], command: input["command"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint5_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint5_15Input = {
readonly sessionID: Endpoint5_15Request["params"]["sessionID"]
readonly id?: Endpoint5_15Request["payload"]["id"]
type Endpoint5_16Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint5_16Input = {
readonly sessionID: Endpoint5_16Request["params"]["sessionID"]
readonly id?: Endpoint5_16Request["payload"]["id"]
}
const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) =>
const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) =>
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint5_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint5_16Input = { readonly sessionID: Endpoint5_16Request["params"]["sessionID"] }
const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) =>
type Endpoint5_17Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint5_17Input = { readonly sessionID: Endpoint5_17Request["params"]["sessionID"] }
const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) =>
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint5_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint5_17Input = {
readonly sessionID: Endpoint5_17Request["params"]["sessionID"]
readonly messageID: Endpoint5_17Request["payload"]["messageID"]
readonly files?: Endpoint5_17Request["payload"]["files"]
type Endpoint5_18Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint5_18Input = {
readonly sessionID: Endpoint5_18Request["params"]["sessionID"]
readonly messageID: Endpoint5_18Request["payload"]["messageID"]
readonly files?: Endpoint5_18Request["payload"]["files"]
}
const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) =>
const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) =>
raw["session.revert.stage"]({
params: { sessionID: input["sessionID"] },
payload: { messageID: input["messageID"], files: input["files"] },
@@ -305,80 +310,80 @@ const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17I
Effect.map((value) => value.data),
)
type Endpoint5_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint5_18Input = { readonly sessionID: Endpoint5_18Request["params"]["sessionID"] }
const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) =>
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint5_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint5_19Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint5_19Input = { readonly sessionID: Endpoint5_19Request["params"]["sessionID"] }
const Endpoint5_19 = (raw: RawClient["server.session"]) => (input: Endpoint5_19Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint5_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint5_20Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint5_20Input = { readonly sessionID: Endpoint5_20Request["params"]["sessionID"] }
const Endpoint5_20 = (raw: RawClient["server.session"]) => (input: Endpoint5_20Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint5_21Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] }
const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21Input) =>
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint5_21Request = Parameters<RawClient["server.session"]["session.pending.list"]>[0]
type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] }
const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21Input) =>
type Endpoint5_22Request = Parameters<RawClient["server.session"]["session.pending.list"]>[0]
type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] }
const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) =>
raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint5_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] }
const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) =>
type Endpoint5_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
type Endpoint5_23Input = { readonly sessionID: Endpoint5_23Request["params"]["sessionID"] }
const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) =>
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint5_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
type Endpoint5_23Input = {
readonly sessionID: Endpoint5_23Request["params"]["sessionID"]
readonly key: Endpoint5_23Request["params"]["key"]
readonly value: Endpoint5_23Request["payload"]["value"]
type Endpoint5_24Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
type Endpoint5_24Input = {
readonly sessionID: Endpoint5_24Request["params"]["sessionID"]
readonly key: Endpoint5_24Request["params"]["key"]
readonly value: Endpoint5_24Request["payload"]["value"]
}
const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) =>
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
raw["session.instructions.entry.put"]({
params: { sessionID: input["sessionID"], key: input["key"] },
payload: { value: input["value"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint5_24Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
type Endpoint5_24Input = {
readonly sessionID: Endpoint5_24Request["params"]["sessionID"]
readonly key: Endpoint5_24Request["params"]["key"]
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
type Endpoint5_25Input = {
readonly sessionID: Endpoint5_25Request["params"]["sessionID"]
readonly key: Endpoint5_25Request["params"]["key"]
}
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.generate"]>[0]
type Endpoint5_25Input = {
readonly sessionID: Endpoint5_25Request["params"]["sessionID"]
readonly prompt: Endpoint5_25Request["payload"]["prompt"]
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.generate"]>[0]
type Endpoint5_26Input = {
readonly sessionID: Endpoint5_26Request["params"]["sessionID"]
readonly prompt: Endpoint5_26Request["payload"]["prompt"]
}
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint5_26Input = {
readonly sessionID: Endpoint5_26Request["params"]["sessionID"]
readonly after?: Endpoint5_26Request["query"]["after"]
readonly follow?: Endpoint5_26Request["query"]["follow"]
type Endpoint5_27Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint5_27Input = {
readonly sessionID: Endpoint5_27Request["params"]["sessionID"]
readonly after?: Endpoint5_27Request["query"]["after"]
readonly follow?: Endpoint5_27Request["query"]["follow"]
}
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
Stream.unwrap(
raw["session.log"]({
params: { sessionID: input["sessionID"] },
@@ -389,22 +394,22 @@ const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26I
),
)
type Endpoint5_27Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] }
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] }
const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint5_29Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint5_29Input = { readonly sessionID: Endpoint5_29Request["params"]["sessionID"] }
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) =>
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint5_29Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint5_29Input = {
readonly sessionID: Endpoint5_29Request["params"]["sessionID"]
readonly messageID: Endpoint5_29Request["params"]["messageID"]
type Endpoint5_30Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint5_30Input = {
readonly sessionID: Endpoint5_30Request["params"]["sessionID"]
readonly messageID: Endpoint5_30Request["params"]["messageID"]
}
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) =>
const Endpoint5_30 = (raw: RawClient["server.session"]) => (input: Endpoint5_30Input) =>
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
@@ -420,23 +425,24 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
switchAgent: Endpoint5_6(raw),
switchModel: Endpoint5_7(raw),
rename: Endpoint5_8(raw),
move: Endpoint5_9(raw),
prompt: Endpoint5_10(raw),
command: Endpoint5_11(raw),
skill: Endpoint5_12(raw),
synthetic: Endpoint5_13(raw),
shell: Endpoint5_14(raw),
compact: Endpoint5_15(raw),
wait: Endpoint5_16(raw),
revert: { stage: Endpoint5_17(raw), clear: Endpoint5_18(raw), commit: Endpoint5_19(raw) },
context: Endpoint5_20(raw),
pending: { list: Endpoint5_21(raw) },
instructions: { entry: { list: Endpoint5_22(raw), put: Endpoint5_23(raw), remove: Endpoint5_24(raw) } },
generate: Endpoint5_25(raw),
log: Endpoint5_26(raw),
interrupt: Endpoint5_27(raw),
background: Endpoint5_28(raw),
message: Endpoint5_29(raw),
archive: Endpoint5_9(raw),
move: Endpoint5_10(raw),
prompt: Endpoint5_11(raw),
command: Endpoint5_12(raw),
skill: Endpoint5_13(raw),
synthetic: Endpoint5_14(raw),
shell: Endpoint5_15(raw),
compact: Endpoint5_16(raw),
wait: Endpoint5_17(raw),
revert: { stage: Endpoint5_18(raw), clear: Endpoint5_19(raw), commit: Endpoint5_20(raw) },
context: Endpoint5_21(raw),
pending: { list: Endpoint5_22(raw) },
instructions: { entry: { list: Endpoint5_23(raw), put: Endpoint5_24(raw), remove: Endpoint5_25(raw) } },
generate: Endpoint5_26(raw),
log: Endpoint5_27(raw),
interrupt: Endpoint5_28(raw),
background: Endpoint5_29(raw),
message: Endpoint5_30(raw),
})
type Endpoint6_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
@@ -736,17 +742,30 @@ const adaptGroup12 = (raw: RawClient["server.credential"]) => ({ update: Endpoin
const Endpoint13_0 = (raw: RawClient["server.project"]) => () =>
raw["project.list"]({}).pipe(Effect.mapError(mapClientError))
type Endpoint13_1Request = Parameters<RawClient["server.project"]["project.current"]>[0]
type Endpoint13_1Input = { readonly location?: Endpoint13_1Request["query"]["location"] }
const Endpoint13_1 = (raw: RawClient["server.project"]) => (input?: Endpoint13_1Input) =>
type Endpoint13_1Request = Parameters<RawClient["server.project"]["project.update"]>[0]
type Endpoint13_1Input = {
readonly projectID: Endpoint13_1Request["params"]["projectID"]
readonly name?: Endpoint13_1Request["payload"]["name"]
readonly icon?: Endpoint13_1Request["payload"]["icon"]
readonly commands?: Endpoint13_1Request["payload"]["commands"]
}
const Endpoint13_1 = (raw: RawClient["server.project"]) => (input: Endpoint13_1Input) =>
raw["project.update"]({
params: { projectID: input["projectID"] },
payload: { name: input["name"], icon: input["icon"], commands: input["commands"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint13_2Request = Parameters<RawClient["server.project"]["project.current"]>[0]
type Endpoint13_2Input = { readonly location?: Endpoint13_2Request["query"]["location"] }
const Endpoint13_2 = (raw: RawClient["server.project"]) => (input?: Endpoint13_2Input) =>
raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint13_2Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
type Endpoint13_2Input = {
readonly projectID: Endpoint13_2Request["params"]["projectID"]
readonly location?: Endpoint13_2Request["query"]["location"]
type Endpoint13_3Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
type Endpoint13_3Input = {
readonly projectID: Endpoint13_3Request["params"]["projectID"]
readonly location?: Endpoint13_3Request["query"]["location"]
}
const Endpoint13_2 = (raw: RawClient["server.project"]) => (input: Endpoint13_2Input) =>
const Endpoint13_3 = (raw: RawClient["server.project"]) => (input: Endpoint13_3Input) =>
raw["project.directories"]({
params: { projectID: input["projectID"] },
query: { location: input["location"] },
@@ -754,8 +773,9 @@ const Endpoint13_2 = (raw: RawClient["server.project"]) => (input: Endpoint13_2I
const adaptGroup13 = (raw: RawClient["server.project"]) => ({
list: Endpoint13_0(raw),
current: Endpoint13_1(raw),
directories: Endpoint13_2(raw),
update: Endpoint13_1(raw),
current: Endpoint13_2(raw),
directories: Endpoint13_3(raw),
})
type Endpoint14_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
@@ -977,21 +997,26 @@ const Endpoint19_0 = (raw: RawClient["server.event"]) => () =>
const adaptGroup19 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint19_0(raw) })
type Endpoint20_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
type Endpoint20_0Request = Parameters<RawClient["server.pty"]["pty.shells"]>[0]
type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
const Endpoint20_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_0Input) =>
raw["pty.shells"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint20_1Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
type Endpoint20_1Input = { readonly location?: Endpoint20_1Request["query"]["location"] }
const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Input) =>
raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint20_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
type Endpoint20_1Input = {
readonly location?: Endpoint20_1Request["query"]["location"]
readonly command?: Endpoint20_1Request["payload"]["command"]
readonly args?: Endpoint20_1Request["payload"]["args"]
readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
readonly title?: Endpoint20_1Request["payload"]["title"]
readonly env?: Endpoint20_1Request["payload"]["env"]
type Endpoint20_2Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
type Endpoint20_2Input = {
readonly location?: Endpoint20_2Request["query"]["location"]
readonly command?: Endpoint20_2Request["payload"]["command"]
readonly args?: Endpoint20_2Request["payload"]["args"]
readonly cwd?: Endpoint20_2Request["payload"]["cwd"]
readonly title?: Endpoint20_2Request["payload"]["title"]
readonly env?: Endpoint20_2Request["payload"]["env"]
}
const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Input) =>
const Endpoint20_2 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_2Input) =>
raw["pty.create"]({
query: { location: input?.["location"] },
payload: {
@@ -1003,46 +1028,61 @@ const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Inpu
},
}).pipe(Effect.mapError(mapClientError))
type Endpoint20_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
type Endpoint20_2Input = {
readonly ptyID: Endpoint20_2Request["params"]["ptyID"]
readonly location?: Endpoint20_2Request["query"]["location"]
type Endpoint20_3Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
type Endpoint20_3Input = {
readonly ptyID: Endpoint20_3Request["params"]["ptyID"]
readonly location?: Endpoint20_3Request["query"]["location"]
}
const Endpoint20_2 = (raw: RawClient["server.pty"]) => (input: Endpoint20_2Input) =>
const Endpoint20_3 = (raw: RawClient["server.pty"]) => (input: Endpoint20_3Input) =>
raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint20_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
type Endpoint20_3Input = {
readonly ptyID: Endpoint20_3Request["params"]["ptyID"]
readonly location?: Endpoint20_3Request["query"]["location"]
readonly title?: Endpoint20_3Request["payload"]["title"]
readonly size?: Endpoint20_3Request["payload"]["size"]
type Endpoint20_4Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
type Endpoint20_4Input = {
readonly ptyID: Endpoint20_4Request["params"]["ptyID"]
readonly location?: Endpoint20_4Request["query"]["location"]
readonly title?: Endpoint20_4Request["payload"]["title"]
readonly size?: Endpoint20_4Request["payload"]["size"]
}
const Endpoint20_3 = (raw: RawClient["server.pty"]) => (input: Endpoint20_3Input) =>
const Endpoint20_4 = (raw: RawClient["server.pty"]) => (input: Endpoint20_4Input) =>
raw["pty.update"]({
params: { ptyID: input["ptyID"] },
query: { location: input["location"] },
payload: { title: input["title"], size: input["size"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint20_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
type Endpoint20_4Input = {
readonly ptyID: Endpoint20_4Request["params"]["ptyID"]
readonly location?: Endpoint20_4Request["query"]["location"]
type Endpoint20_5Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
type Endpoint20_5Input = {
readonly ptyID: Endpoint20_5Request["params"]["ptyID"]
readonly location?: Endpoint20_5Request["query"]["location"]
}
const Endpoint20_4 = (raw: RawClient["server.pty"]) => (input: Endpoint20_4Input) =>
const Endpoint20_5 = (raw: RawClient["server.pty"]) => (input: Endpoint20_5Input) =>
raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint20_6Request = Parameters<RawClient["server.pty"]["pty.connectToken"]>[0]
type Endpoint20_6Input = {
readonly ptyID: Endpoint20_6Request["params"]["ptyID"]
readonly location?: Endpoint20_6Request["query"]["location"]
readonly "x-opencode-ticket"?: Endpoint20_6Request["headers"]["x-opencode-ticket"]
}
const Endpoint20_6 = (raw: RawClient["server.pty"]) => (input: Endpoint20_6Input) =>
raw["pty.connectToken"]({
params: { ptyID: input["ptyID"] },
query: { location: input["location"] },
headers: { "x-opencode-ticket": input["x-opencode-ticket"] },
}).pipe(Effect.mapError(mapClientError))
const adaptGroup20 = (raw: RawClient["server.pty"]) => ({
list: Endpoint20_0(raw),
create: Endpoint20_1(raw),
get: Endpoint20_2(raw),
update: Endpoint20_3(raw),
remove: Endpoint20_4(raw),
shells: Endpoint20_0(raw),
list: Endpoint20_1(raw),
create: Endpoint20_2(raw),
get: Endpoint20_3(raw),
update: Endpoint20_4(raw),
remove: Endpoint20_5(raw),
connectToken: Endpoint20_6(raw),
})
type Endpoint21_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
@@ -1214,34 +1254,50 @@ const adaptGroup24 = (raw: RawClient["server.projectCopy"]) => ({
refresh: Endpoint24_2(raw),
})
type Endpoint25_0Request = Parameters<RawClient["server.vcs"]["vcs.status"]>[0]
type Endpoint25_0Request = Parameters<RawClient["server.vcs"]["vcs.get"]>[0]
type Endpoint25_0Input = { readonly location?: Endpoint25_0Request["query"]["location"] }
const Endpoint25_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_0Input) =>
raw["vcs.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint25_1Request = Parameters<RawClient["server.vcs"]["vcs.status"]>[0]
type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_1Input) =>
raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint25_1Request = Parameters<RawClient["server.vcs"]["vcs.diff"]>[0]
type Endpoint25_1Input = {
readonly location?: Endpoint25_1Request["query"]["location"]
readonly mode: Endpoint25_1Request["query"]["mode"]
readonly context?: Endpoint25_1Request["query"]["context"]
type Endpoint25_2Request = Parameters<RawClient["server.vcs"]["vcs.diff"]>[0]
type Endpoint25_2Input = {
readonly location?: Endpoint25_2Request["query"]["location"]
readonly mode: Endpoint25_2Request["query"]["mode"]
readonly context?: Endpoint25_2Request["query"]["context"]
}
const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_1Input) =>
const Endpoint25_2 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_2Input) =>
raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe(
Effect.mapError(mapClientError),
)
const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint25_0(raw), diff: Endpoint25_1(raw) })
const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({
get: Endpoint25_0(raw),
status: Endpoint25_1(raw),
diff: Endpoint25_2(raw),
})
const Endpoint26_0 = (raw: RawClient["server.debug"]) => () =>
type Endpoint26_0Request = Parameters<RawClient["server.path"]["path.get"]>[0]
type Endpoint26_0Input = { readonly location?: Endpoint26_0Request["query"]["location"] }
const Endpoint26_0 = (raw: RawClient["server.path"]) => (input?: Endpoint26_0Input) =>
raw["path.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup26 = (raw: RawClient["server.path"]) => ({ get: Endpoint26_0(raw) })
const Endpoint27_0 = (raw: RawClient["server.debug"]) => () =>
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
type Endpoint26_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] }
const Endpoint26_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint26_1Input) =>
type Endpoint27_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
type Endpoint27_1Input = { readonly location?: Endpoint27_1Request["query"]["location"] }
const Endpoint27_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint27_1Input) =>
raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup26 = (raw: RawClient["server.debug"]) => ({
location: { list: Endpoint26_0(raw), evict: Endpoint26_1(raw) },
const adaptGroup27 = (raw: RawClient["server.debug"]) => ({
location: { list: Endpoint27_0(raw), evict: Endpoint27_1(raw) },
})
const adaptClient = (raw: RawClient) => ({
@@ -1271,7 +1327,8 @@ const adaptClient = (raw: RawClient) => ({
reference: adaptGroup23(raw["server.reference"]),
projectCopy: adaptGroup24(raw["server.projectCopy"]),
vcs: adaptGroup25(raw["server.vcs"]),
debug: adaptGroup26(raw["server.debug"]),
path: adaptGroup26(raw["server.path"]),
debug: adaptGroup27(raw["server.debug"]),
})
export const make = (options?: { readonly baseUrl?: URL | string }) =>
+9
View File
@@ -3,13 +3,22 @@ type Client = ReturnType<typeof import("./generated/client.js").make>
export type AgentApi = Client["agent"]
export type CommandApi = Client["command"]
export type EventApi = Client["event"]
export type FileApi = Client["file"]
export type IntegrationApi = Client["integration"]
export type MessageApi = Client["message"]
export type McpApi = Client["mcp"]
export type ModelApi = Client["model"]
export type PluginApi = Client["plugin"]
export type PermissionApi = Client["permission"]
export type PathApi = Client["path"]
export type ProjectApi = Client["project"]
export type ProviderApi = Client["provider"]
export type QuestionApi = Client["question"]
export type ReferenceApi = Client["reference"]
export type SessionApi = Client["session"]
export type SkillApi = Client["skill"]
export type PtyApi = Client["pty"]
export type VcsApi = Client["vcs"]
export interface CatalogApi {
readonly provider: ProviderApi
@@ -26,6 +26,8 @@ import type {
SessionSwitchModelOutput,
SessionRenameInput,
SessionRenameOutput,
SessionArchiveInput,
SessionArchiveOutput,
SessionMoveInput,
SessionMoveOutput,
SessionPromptInput,
@@ -119,6 +121,8 @@ import type {
CredentialRemoveInput,
CredentialRemoveOutput,
ProjectListOutput,
ProjectUpdateInput,
ProjectUpdateOutput,
ProjectCurrentInput,
ProjectCurrentOutput,
ProjectDirectoriesInput,
@@ -162,6 +166,8 @@ import type {
SkillListInput,
SkillListOutput,
EventSubscribeOutput,
PtyShellsInput,
PtyShellsOutput,
PtyListInput,
PtyListOutput,
PtyCreateInput,
@@ -172,6 +178,8 @@ import type {
PtyUpdateOutput,
PtyRemoveInput,
PtyRemoveOutput,
PtyConnectTokenInput,
PtyConnectTokenOutput,
ShellListInput,
ShellListOutput,
ShellCreateInput,
@@ -200,10 +208,14 @@ import type {
ProjectCopyRemoveOutput,
ProjectCopyRefreshInput,
ProjectCopyRefreshOutput,
VcsGetInput,
VcsGetOutput,
VcsStatusInput,
VcsStatusOutput,
VcsDiffInput,
VcsDiffOutput,
PathGetInput,
PathGetOutput,
DebugLocationListOutput,
DebugLocationEvictInput,
DebugLocationEvictOutput,
@@ -535,6 +547,17 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
archive: (input: SessionArchiveInput, requestOptions?: RequestOptions) =>
request<SessionArchiveOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/archive`,
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
},
requestOptions,
),
move: (input: SessionMoveInput, requestOptions?: RequestOptions) =>
request<SessionMoveOutput>(
{
@@ -1149,6 +1172,18 @@ export function make(options: ClientOptions) {
{ method: "GET", path: `/api/project`, successStatus: 200, declaredStatuses: [401, 400], empty: false },
requestOptions,
),
update: (input: ProjectUpdateInput, requestOptions?: RequestOptions) =>
request<ProjectUpdateOutput>(
{
method: "PATCH",
path: `/api/project/${encodeURIComponent(input.projectID)}`,
body: { name: input["name"], icon: input["icon"], commands: input["commands"] },
successStatus: 200,
declaredStatuses: [404, 401, 400],
empty: false,
},
requestOptions,
),
current: (input?: ProjectCurrentInput, requestOptions?: RequestOptions) =>
request<ProjectCurrentOutput>(
{
@@ -1428,6 +1463,18 @@ export function make(options: ClientOptions) {
),
},
pty: {
shells: (input?: PtyShellsInput, requestOptions?: RequestOptions) =>
request<PtyShellsOutput>(
{
method: "GET",
path: `/api/pty/shells`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
list: (input?: PtyListInput, requestOptions?: RequestOptions) =>
request<PtyListOutput>(
{
@@ -1496,6 +1543,19 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
connectToken: (input: PtyConnectTokenInput, requestOptions?: RequestOptions) =>
request<PtyConnectTokenOutput>(
{
method: "POST",
path: `/api/pty/${encodeURIComponent(input.ptyID)}/connect-token`,
query: { location: input["location"] },
headers: { "x-opencode-ticket": input["x-opencode-ticket"] },
successStatus: 200,
declaredStatuses: [403, 404, 401, 400],
empty: false,
},
requestOptions,
),
},
shell: {
list: (input?: ShellListInput, requestOptions?: RequestOptions) =>
@@ -1683,6 +1743,18 @@ export function make(options: ClientOptions) {
),
},
vcs: {
get: (input?: VcsGetInput, requestOptions?: RequestOptions) =>
request<VcsGetOutput>(
{
method: "GET",
path: `/api/vcs`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
status: (input?: VcsStatusInput, requestOptions?: RequestOptions) =>
request<VcsStatusOutput>(
{
@@ -1708,6 +1780,20 @@ export function make(options: ClientOptions) {
requestOptions,
),
},
path: {
get: (input?: PathGetInput, requestOptions?: RequestOptions) =>
request<PathGetOutput>(
{
method: "GET",
path: `/api/path`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
debug: {
location: {
list: (requestOptions?: RequestOptions) =>
+102 -1
View File
@@ -8,7 +8,7 @@ export type ModelRef = { id: string; providerID: string; variant?: string }
export type ProviderSettings = { [x: string]: JsonValue }
export type AgentColor = string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info"
export type AgentColor = string
export type PermissionV2Effect = "allow" | "deny" | "ask"
@@ -501,6 +501,10 @@ export type QuestionTool = { messageID: string; callID: string }
export type QuestionAnswer = Array<string>
export type PtyShell = { path: string; name: string; acceptable: boolean }
export type PtyTicketConnectToken = { ticket: string; expires_in: number }
export type ShellInfo1 = {
id: string
status: "running" | "exited" | "timeout" | "killed"
@@ -526,6 +530,8 @@ export type ReferenceGitSource = {
export type ProjectCopyCopy = { directory: string }
export type VcsInfo = { branch?: string; defaultBranch?: string }
export type VcsFileStatus = {
file: string
additions: number
@@ -533,6 +539,8 @@ export type VcsFileStatus = {
status: "added" | "deleted" | "modified"
}
export type PathInfo = { home: string; state: string; config: string; worktree: string; directory: string }
export type SessionMessageModelSelected = {
id: string
metadata?: { [x: string]: JsonValue }
@@ -549,6 +557,7 @@ export type CommandInfo = {
agent?: string
model?: ModelRef
subtask?: boolean
source?: "command" | "mcp"
}
export type ProviderRequest = {
@@ -599,6 +608,16 @@ export type SessionRenamed = {
data: { sessionID: string; title: string }
}
export type SessionArchived = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.archived"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string }
}
export type SessionDeleted = {
id: string
created: number
@@ -2202,6 +2221,7 @@ export type SessionEventDurable =
| SessionModelSelected
| SessionMoved
| SessionRenamed
| SessionArchived
| SessionDeleted
| SessionForked
| SessionInputPromoted
@@ -2296,6 +2316,7 @@ export type V2Event =
| SessionModelSelected
| SessionMoved
| SessionRenamed
| SessionArchived
| SessionUsageUpdated
| SessionDeleted
| SessionForked
@@ -2495,6 +2516,14 @@ export type McpServerNotFoundError = {
export const isMcpServerNotFoundError = (value: unknown): value is McpServerNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "McpServerNotFoundError"
export type ProjectNotFoundError = {
readonly _tag: "ProjectNotFoundError"
readonly projectID: string
readonly message: string
}
export const isProjectNotFoundError = (value: unknown): value is ProjectNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProjectNotFoundError"
export type FormNotFoundError = { readonly _tag: "FormNotFoundError"; readonly id: string; readonly message: string }
export const isFormNotFoundError = (value: unknown): value is FormNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormNotFoundError"
@@ -2527,6 +2556,10 @@ export type PtyNotFoundError = { readonly _tag: "PtyNotFoundError"; readonly pty
export const isPtyNotFoundError = (value: unknown): value is PtyNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PtyNotFoundError"
export type ForbiddenError = { readonly _tag: "ForbiddenError"; readonly message: string }
export const isForbiddenError = (value: unknown): value is ForbiddenError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ForbiddenError"
export type ShellNotFoundError = { readonly _tag: "ShellNotFoundError"; readonly id: string; readonly message: string }
export const isShellNotFoundError = (value: unknown): value is ShellNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ShellNotFoundError"
@@ -2757,6 +2790,10 @@ export type SessionRenameInput = {
export type SessionRenameOutput = void
export type SessionArchiveInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type SessionArchiveOutput = void
export type SessionMoveInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly directory: { readonly directory: string; readonly workspaceID?: string }["directory"]
@@ -3584,6 +3621,27 @@ export type CredentialRemoveOutput = void
export type ProjectListOutput = Array<Project>
export type ProjectUpdateInput = {
readonly projectID: { readonly projectID: string }["projectID"]
readonly name?: {
readonly name?: string
readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string }
readonly commands?: { readonly start?: string }
}["name"]
readonly icon?: {
readonly name?: string
readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string }
readonly commands?: { readonly start?: string }
}["icon"]
readonly commands?: {
readonly name?: string
readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string }
readonly commands?: { readonly start?: string }
}["commands"]
}
export type ProjectUpdateOutput = Project
export type ProjectCurrentInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@@ -4646,6 +4704,17 @@ export type SkillListOutput = {
export type EventSubscribeOutput = V2Event
export type PtyShellsInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type PtyShellsOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<PtyShell>
}
export type PtyListInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@@ -4741,6 +4810,19 @@ export type PtyRemoveInput = {
export type PtyRemoveOutput = void
export type PtyConnectTokenInput = {
readonly ptyID: { readonly ptyID: string }["ptyID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly "x-opencode-ticket"?: { readonly "x-opencode-ticket"?: string | undefined }["x-opencode-ticket"]
}
export type PtyConnectTokenOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: PtyTicketConnectToken
}
export type ShellListInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@@ -4918,6 +5000,17 @@ export type ProjectCopyRefreshInput = {
export type ProjectCopyRefreshOutput = void
export type VcsGetInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type VcsGetOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: VcsInfo
}
export type VcsStatusInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@@ -4952,6 +5045,14 @@ export type VcsDiffOutput = {
data: Array<FileDiffInfo>
}
export type PathGetInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type PathGetOutput = PathInfo
export type DebugLocationListOutput = Array<LocationRef>
export type DebugLocationEvictInput = {
+9
View File
@@ -4,13 +4,22 @@ export type {
CatalogApi,
CommandApi,
EventApi,
FileApi,
IntegrationApi,
MessageApi,
McpApi,
ModelApi,
PermissionApi,
PathApi,
PluginApi,
ProjectApi,
ProviderApi,
QuestionApi,
ReferenceApi,
SessionApi,
SkillApi,
PtyApi,
VcsApi,
} from "./api.js"
export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types"
export type OpenCodeClient = ReturnType<typeof import("./generated/client").make>
-3
View File
@@ -37,7 +37,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
let announced = false
let lastSpawn = 0
let spawnDelay = 5_000
let ownerHeld = false
const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) => {
if (announced) return
@@ -65,7 +64,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
const registration = await registered(options.file, true)
if (registration.service !== undefined) {
ownerHeld = false
spawnDelay = 5_000
const service = registration.service
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
@@ -82,7 +80,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
if (failure !== undefined) throw failure
const finished = [...contenders].filter(contenderFinished)
if (finished.some((item) => item.child.exitCode === 0)) {
ownerHeld = true
spawnDelay = Math.min(spawnDelay * 2, 30_000)
}
finished.forEach((item) => contenders.delete(item))
+33 -3
View File
@@ -31,6 +31,7 @@ test("exposes every standard HTTP API group", () => {
"reference",
"projectCopy",
"vcs",
"path",
"debug",
])
expect(Object.keys(client.debug)).toEqual(["location"])
@@ -42,10 +43,37 @@ test("exposes every standard HTTP API group", () => {
expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"])
expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"])
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
expect(Object.keys(client.vcs)).toEqual(["get", "status", "diff"])
expect(Object.keys(client.pty)).toEqual(["shells", "list", "create", "get", "update", "remove", "connectToken"])
expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"])
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
expect(Object.keys(client.project)).toEqual(["list", "update", "current", "directories"])
})
test("PTY connect token sends the required browser header", async () => {
let request: Request | undefined
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input, init) => {
request = input instanceof Request ? input : new Request(input, init)
return Response.json({
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
data: { ticket: "ticket", expires_in: 60 },
})
},
})
const result = await client.pty.connectToken({
ptyID: "pty_test",
location: { directory: "/tmp/project" },
"x-opencode-ticket": "1",
})
expect(result.data.ticket).toBe("ticket")
expect(request?.method).toBe("POST")
expect(request?.headers.get("x-opencode-ticket")).toBe("1")
expect(request?.url).toBe(
"http://localhost:3000/api/pty/pty_test/connect-token?location%5Bdirectory%5D=%2Ftmp%2Fproject",
)
})
test("server.get uses the public HTTP contract", async () => {
@@ -399,6 +427,7 @@ test("session methods use the public HTTP contract", async () => {
sessionID: "ses_test",
model: { id: "claude", providerID: "anthropic" },
})
await client.session.archive({ sessionID: "ses_test" })
const admitted = await client.session.prompt({
sessionID: "ses_test",
text: "Hello",
@@ -434,6 +463,7 @@ test("session methods use the public HTTP contract", async () => {
["POST", "http://localhost:3000/api/session"],
["POST", "http://localhost:3000/api/session/ses_test/agent"],
["POST", "http://localhost:3000/api/session/ses_test/model"],
["POST", "http://localhost:3000/api/session/ses_test/archive"],
["POST", "http://localhost:3000/api/session/ses_test/prompt"],
["POST", "http://localhost:3000/api/session/ses_test/generate"],
["POST", "http://localhost:3000/api/session/ses_test/synthetic"],
+3 -19
View File
@@ -10561,26 +10561,10 @@
"additionalProperties": false
},
"Agent.Color": {
"anyOf": [
"type": "string",
"allOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^#[0-9a-fA-F]{6}$"
}
]
},
{
"type": "string",
"enum": [
"primary",
"secondary",
"accent",
"success",
"warning",
"error",
"info"
]
"pattern": "^#[0-9a-fA-F]{6}$"
}
]
},
+5 -1
View File
@@ -85,6 +85,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
name: mcpCommandName(prompt.server, prompt.name),
template: "",
description: prompt.description,
source: "mcp",
}),
)
})
@@ -98,7 +99,10 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
return (yield* mcpCommands()).find((command) => command.name === name)
}),
list: Effect.fn("CommandV2.list")(function* () {
const commands = Array.from(state.get().commands.values()) as Info[]
const commands = Array.from(state.get().commands.values()).map((command) => ({
...command,
source: command.source ?? "command" as const,
}))
const names = new Set(commands.map((command) => command.name))
return [
...commands,
+1 -4
View File
@@ -6,10 +6,7 @@ import { ConfigProvider } from "./provider"
import { ConfigModel } from "./model"
import { PositiveInt } from "../schema"
export const Color = Schema.Union([
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
])
export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
model: ConfigModel.Selection.pipe(Schema.optional),
+30 -50
View File
@@ -2,12 +2,10 @@ export * as Generate from "./generate"
import { LLM, LLMClient, LLMError } from "@opencode-ai/ai"
import { Context, Effect, Layer, Schema } from "effect"
import { Catalog } from "./catalog"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { llmClient } from "./effect/app-node-platform"
import { Integration } from "./integration"
import { ModelResolver } from "./model-resolver"
import { ModelV2 } from "./model"
import { SessionRunnerModel } from "./session/runner/model"
export interface TextInput {
readonly prompt: string
@@ -19,10 +17,10 @@ export class ModelSelectionError extends Schema.TaggedErrorClass<ModelSelectionE
{ message: Schema.String },
) {}
export class UnavailableError extends Schema.TaggedErrorClass<UnavailableError>()(
"Generate.UnavailableError",
{ message: Schema.String, service: Schema.optional(Schema.String) },
) {}
export class UnavailableError extends Schema.TaggedErrorClass<UnavailableError>()("Generate.UnavailableError", {
message: Schema.String,
service: Schema.optional(Schema.String),
}) {}
export type Error = ModelSelectionError | UnavailableError
@@ -35,56 +33,34 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
const llm = yield* LLMClient.Service
const selectModel = Effect.fn("Generate.selectModel")(function* (requested?: ModelV2.Ref) {
const selected = requested
? yield* catalog.model.get(requested.providerID, requested.id)
: yield* catalog.model.default().pipe(
Effect.flatMap((model) =>
model && SessionRunnerModel.supported(model)
? Effect.succeed(model)
: Effect.map(catalog.model.available(), (models) => models.find(SessionRunnerModel.supported)),
),
)
if (!selected)
return yield* new ModelSelectionError({
message: requested
? `Model unavailable: ${requested.providerID}/${requested.id}`
: "No model specified and no supported model is available",
})
return yield* SessionRunnerModel.withVariant(selected, requested?.variant).pipe(
Effect.mapError(
() =>
new ModelSelectionError({
message: `Variant unavailable for ${selected.providerID}/${selected.id}: ${requested?.variant}`,
}),
),
)
})
const resolver = yield* ModelResolver.Service
const runText = Effect.fn("Generate.text")(function* (input: TextInput) {
const selected = yield* selectModel(input.model)
const provider = yield* catalog.provider.get(selected.providerID)
const connection = yield* integrations.connection.active(
provider?.integrationID ?? Integration.ID.make(selected.providerID),
const resolved = yield* resolver.resolve(input.model).pipe(
Effect.catchTags({
"SessionRunnerModel.VariantUnavailableError": (error) =>
input.model
? new ModelSelectionError({ message: error.message })
: new UnavailableError({ message: error.message, service: error.providerID }),
"SessionRunnerModel.UnsupportedPackageError": (error) =>
input.model
? new ModelSelectionError({ message: error.message })
: new UnavailableError({ message: error.message, service: error.providerID }),
}),
)
const credential = connection ? yield* integrations.connection.resolve(connection) : undefined
const model = yield* SessionRunnerModel.fromCatalogModel(selected, credential).pipe(
Effect.mapError((error) =>
input.model
? new ModelSelectionError({ message: error.message })
: new UnavailableError({ message: error.message, service: selected.providerID }),
),
)
const response = yield* llm.generate(LLM.request({ model, prompt: input.prompt })).pipe(
if (!resolved)
return yield* new ModelSelectionError({
message: input.model
? `Model unavailable: ${input.model.providerID}/${input.model.id}`
: "No model specified and no supported model is available",
})
const response = yield* llm.generate(LLM.request({ model: resolved.model, prompt: input.prompt })).pipe(
Effect.mapError(
(error: LLMError) =>
new UnavailableError({
message: error.message,
service: selected.providerID,
service: resolved.ref.providerID,
}),
),
)
@@ -106,4 +82,8 @@ export const layer = Layer.effect(
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, Integration.node, llmClient] })
export const node = makeLocationNode({
service: Service,
layer,
deps: [ModelResolver.node, llmClient],
})
+2
View File
@@ -20,6 +20,7 @@ import { Integration } from "./integration"
import { Location } from "./location"
import { LocationMutation } from "./location-mutation"
import { LocationServiceMap } from "./location-service-map"
import { ModelResolver } from "./model-resolver"
import { MCP } from "./mcp/index"
import { PermissionV2 } from "./permission"
import { PluginV2 } from "./plugin"
@@ -58,6 +59,7 @@ const locationServiceNodes = [
Reference.node,
Integration.node,
Catalog.node,
ModelResolver.node,
AISDK.node,
PluginV2.node,
PluginSupervisor.node,
+344
View File
@@ -0,0 +1,344 @@
export * as ModelResolver from "./model-resolver"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Model } from "@opencode-ai/ai"
// ast-grep-ignore: no-star-import
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
// ast-grep-ignore: no-star-import
import * as OpenAICompatibleChat from "@opencode-ai/ai/protocols/openai-compatible-chat"
// ast-grep-ignore: no-star-import
import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses"
import { Auth, type AnyRoute } from "@opencode-ai/ai/route"
import { Context, Effect, Layer, Schema } from "effect"
import { produce } from "immer"
import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
import { Credential } from "./credential"
import { Integration } from "./integration"
import { ModelV2 } from "./model"
import { Npm } from "@opencode-ai/util/npm"
import { OpenAICodex } from "./plugin/provider/openai-codex"
import { ProviderV2 } from "./provider"
export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()(
"SessionRunnerModel.VariantUnavailableError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
variant: ModelV2.VariantID,
},
) {
override get message() {
return `Variant unavailable for ${this.providerID}/${this.modelID}: ${this.variant}`
}
}
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
"SessionRunnerModel.UnsupportedPackageError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
package: Schema.String,
},
) {
override get message() {
return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}`
}
}
export type Error = VariantUnavailableError | UnsupportedPackageError | Integration.AuthorizationError
export interface Resolved {
/** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */
readonly model: Model
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
readonly ref: ModelV2.Ref
/** Catalog capabilities used to shape requests before provider lowering. */
readonly capabilities: ModelV2.Capabilities
/** Catalog pricing in dollars per million tokens. */
readonly cost: ModelV2.Info["cost"]
}
export interface Interface {
readonly resolve: (requested?: ModelV2.Ref) => Effect.Effect<Resolved | undefined, Error>
readonly resolveModel: (model: ModelV2.Info, variant?: ModelV2.VariantID) => Effect.Effect<Resolved, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ModelResolver") {}
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
if (credential?.type === "key") return Auth.value(credential.key)
if (credential?.type === "oauth") return Auth.value(credential.access)
const value = model.settings?.apiKey
if (typeof value === "string") return Auth.value(value)
return undefined
}
const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
route.with({
provider: model.providerID,
endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
headers: providerHeaders(model),
providerOptions: providerOptions(model),
http: model.body === undefined ? undefined : { body: model.body },
limits: { context: model.limit.context, output: model.limit.output },
})
const providerHeaders = (model: ModelV2.Info) => {
const packageName = ProviderV2.packageName(model.package)
const generated = new Map<string, string>()
if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string")
generated.set("OpenAI-Organization", model.settings.organization)
if (packageName === "@ai-sdk/openai" && typeof model.settings?.project === "string")
generated.set("OpenAI-Project", model.settings.project)
if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string")
generated.set("Authorization", `Bearer ${model.settings.authToken}`)
return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
}
const providerOptions = (
model: ModelV2.Info,
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined
const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings
if (Object.keys(settings).length === 0) return undefined
const packageName = ProviderV2.packageName(model.package)
if (packageName === "@ai-sdk/openai") return { openai: settings }
if (packageName === "@ai-sdk/anthropic") return { anthropic: settings }
if (packageName === "@ai-sdk/openai-compatible") return { openai: settings }
return undefined
}
export const withVariant = (
model: ModelV2.Info,
variantID: ModelV2.VariantID | undefined,
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
const id = variantID === "default" ? undefined : variantID
const variant = model.variants?.find((item) => item.id === id)
if (!variant && variantID !== undefined && variantID !== "default")
return Effect.fail(
new VariantUnavailableError({
providerID: model.providerID,
modelID: model.id,
variant: variantID,
}),
)
return Effect.succeed(
variant
? produce(model, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings)
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
})
: model,
)
}
export interface Dependencies {
readonly loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError>
readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect<Model, AISDK.InitError>
}
export const fromCatalogModel = (
model: ModelV2.Info,
credential?: Credential.Value,
dependencies?: Dependencies,
): Effect.Effect<Model, UnsupportedPackageError> => {
const resolved = produce(model, (draft) => {
if (draft.settings?.apiKey === "") delete draft.settings.apiKey
if (credential?.type === "key" && credential.metadata !== undefined)
draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata)
})
const packageName = ProviderV2.packageName(resolved.package)
const key = apiKey(resolved, credential)
if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
return Effect.succeed(codexModel(resolved, credential, key))
}
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
return Effect.succeed(
withDefaults(resolved, OpenAIResponses.route)
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
return Effect.succeed(
withDefaults(resolved, AnthropicMessages.route)
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (
ProviderV2.isAISDK(resolved.package) &&
packageName === "@ai-sdk/openai-compatible" &&
typeof resolved.settings?.baseURL === "string"
) {
return Effect.succeed(
withDefaults(resolved, OpenAICompatibleChat.route)
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (ProviderV2.isAISDK(resolved.package)) {
if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved))
const runtime = produce(resolved, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, {
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
...credential?.metadata,
})
})
return dependencies.loadAISDK(runtime).pipe(Effect.mapError(() => unsupported(resolved)))
}
if (!resolved.package) return Effect.fail(unsupported(resolved))
const specifier = resolved.package
return Effect.gen(function* () {
const module = yield* (dependencies?.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe(
Effect.mapError(() => unsupported(resolved)),
)
const configured = { ...resolved.settings, ...credential?.metadata }
const settings = {
...(credential ? withoutNativeAuthSettings(configured) : configured),
...nativeCredentialSettings(specifier, credential),
headers: resolved.headers,
body: resolved.body,
limits: { context: resolved.limit.context, output: resolved.limit.output },
}
return yield* Effect.try({
try: () => {
const runtime = module.model(resolved.modelID ?? resolved.id, settings)
return Model.update(runtime, {
provider: resolved.providerID,
compatibility: resolved.compatibility
? Object.assign({}, runtime.compatibility, resolved.compatibility)
: runtime.compatibility,
})
},
catch: () => unsupported(resolved),
})
})
}
const isNativeOpenAI = (packageName: string | undefined) =>
packageName === "@opencode-ai/ai/providers/openai" ||
packageName?.startsWith("@opencode-ai/ai/providers/openai/") === true
const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => {
if (!credential) return {}
if (credential.type === "key") return { apiKey: credential.key }
if (
specifier === "@opencode-ai/ai/providers/anthropic" ||
specifier === "@opencode-ai/ai/providers/anthropic-compatible"
)
return { authToken: credential.access }
if (
specifier === "@opencode-ai/ai/providers/google-vertex" ||
specifier.startsWith("@opencode-ai/ai/providers/google-vertex/")
)
return { accessToken: credential.access }
return { apiKey: credential.access }
}
const withoutNativeAuthSettings = (settings: Record<string, unknown>) => {
const { accessToken: _accessToken, apiKey: _apiKey, authToken: _authToken, ...rest } = settings
return rest
}
const codexModel = (
model: ModelV2.Info,
credential: Credential.Value | undefined,
key: ReturnType<typeof Auth.value> | undefined,
) => {
const account = OpenAICodex.accountID(credential)
return withDefaults(model, OpenAIResponses.route)
.with({
endpoint: { baseURL: OpenAICodex.baseURL },
auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen(
account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }),
),
})
.model({ id: model.modelID ?? model.id, compatibility: model.compatibility })
}
const unsupported = (model: ModelV2.Info) =>
new UnsupportedPackageError({
providerID: model.providerID,
modelID: model.id,
package: model.package ?? "unknown",
})
export const resolveModel = (
model: ModelV2.Info,
variant: ModelV2.VariantID | undefined,
credential?: Credential.Value,
dependencies?: Dependencies,
) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)))
export const supported = (model: ModelV2.Info) => Boolean(model.package)
/** Resolves catalog selections into runtime models for the current Location. */
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
const npm = yield* Npm.Service
const aisdk = yield* AISDK.Service
const load = Effect.fn("ModelResolver.resolveModel")(function* (
selected: ModelV2.Info,
variant?: ModelV2.VariantID,
) {
const provider = yield* catalog.provider.get(selected.providerID)
const connection = yield* integrations.connection.active(
provider?.integrationID ?? Integration.ID.make(selected.providerID),
)
const model = yield* resolveModel(
selected,
variant,
connection ? yield* integrations.connection.resolve(connection) : undefined,
{
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
loadAISDK: (model) => aisdk.model(model),
},
)
return {
model,
ref: ModelV2.Ref.make({
id: selected.id,
providerID: selected.providerID,
...(variant === undefined ? {} : { variant }),
}),
capabilities: selected.capabilities,
cost: selected.cost,
}
})
return Service.of({
resolve: Effect.fn("ModelResolver.resolve")(function* (requested) {
const selected = requested
? yield* catalog.model.get(requested.providerID, requested.id)
: yield* catalog.model
.default()
.pipe(
Effect.flatMap((model) =>
model && supported(model)
? Effect.succeed(model)
: Effect.map(catalog.model.available(), (models) => models.find(supported)),
),
)
if (!selected) return undefined
return yield* load(selected, requested?.variant)
}),
resolveModel: load,
})
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [Catalog.node, Integration.node, Npm.node, AISDK.node],
})
@@ -1,7 +1,7 @@
export * as OpenAICodex from "./openai-codex"
// TEMPORARY SEAM (#34765): plugins have no hook into LLM route construction, so
// codex routing lives in SessionRunnerModel.fromCatalogModel and catalog filtering
// Codex routing lives in ModelResolver and catalog filtering.
// in OpenAIPlugin, sharing this module. Once the native provider packages land
// (#33689/#33925/#34462) this should collapse into the native OpenAI provider.
// The eligibility rules mirror V1's CodexAuthPlugin allowlist; models.dev has no
+29 -2
View File
@@ -3,7 +3,7 @@ export * as Project from "./project"
import { Context, Effect, Layer, Schema } from "effect"
import { ChildProcess } from "effect/unstable/process"
import { asc, desc } from "drizzle-orm"
import { asc, desc, eq } from "drizzle-orm"
import path from "path"
import { AbsolutePath } from "./schema"
import { Database } from "./database/database"
@@ -37,6 +37,13 @@ export type DirectoriesInput = typeof DirectoriesInput.Type
export const Directories = ProjectSchema.Directories
export type Directories = typeof Directories.Type
export const UpdateInput = ProjectSchema.UpdateInput
export type UpdateInput = typeof UpdateInput.Type
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Project.NotFoundError", {
projectID: ID,
}) {}
export interface Resolved {
readonly previous?: ID
readonly id: ID
@@ -58,6 +65,7 @@ export const root = Effect.fn("Project.root")(function* (
export interface Interface {
readonly list: () => Effect.Effect<ReadonlyArray<Info>>
readonly directories: (input: DirectoriesInput) => Effect.Effect<Directories>
readonly update: (projectID: ID, input: UpdateInput) => Effect.Effect<Info, NotFoundError>
readonly resolve: (input: AbsolutePath) => Effect.Effect<Resolved>
/**
* Temporary bridge method for writing the resolved project ID to the repo-local cache.
@@ -121,6 +129,25 @@ const layer = Layer.effect(
return yield* projectDirectories.list(input.projectID)
})
const update = Effect.fn("Project.update")(function* (projectID: ID, input: UpdateInput) {
const row = yield* db
.update(ProjectTable)
.set({
name: input.name,
icon_url: input.icon?.url,
icon_url_override: input.icon?.override,
icon_color: input.icon?.color,
commands: input.commands,
time_updated: Date.now(),
})
.where(eq(ProjectTable.id, projectID))
.returning()
.get()
.pipe(Effect.orDie)
if (!row) return yield* new NotFoundError({ projectID })
return fromRow(row)
})
const cached = Effect.fnUntraced(function* (dir: string) {
return yield* fs.readFileString(path.join(dir, "opencode")).pipe(
Effect.map((value) => value.trim()),
@@ -229,7 +256,7 @@ const layer = Layer.effect(
yield* fs.writeFileString(path.join(input.store, "opencode"), input.id).pipe(Effect.ignore)
})
return Service.of({ list, directories, resolve, commit })
return Service.of({ list, directories, update, resolve, commit })
}),
)
+3
View File
@@ -22,6 +22,9 @@ export type DirectoriesInput = typeof DirectoriesInput.Type
export const Directories = Project.Directories
export type Directories = typeof Directories.Type
export const UpdateInput = Project.UpdateInput
export type UpdateInput = typeof UpdateInput.Type
export const Vcs = Schema.Union([
Schema.Struct({
type: Schema.Literal("git"),
+6 -1
View File
@@ -78,6 +78,7 @@ export class ExitedError extends Schema.TaggedErrorClass<ExitedError>()("Pty.Exi
}) {}
export interface Interface {
readonly shells: () => Effect.Effect<ReadonlyArray<Pty.Shell>>
readonly list: () => Effect.Effect<Info[]>
readonly get: (id: PtyID) => Effect.Effect<Info, NotFoundError>
readonly create: (input: CreateInput) => Effect.Effect<Info>
@@ -158,6 +159,10 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
return Array.from(sessions.values()).map((session) => session.info)
})
const shells = Effect.fn("Pty.shells")(function* () {
return yield* Effect.promise(() => ShellSelect.list(options))
})
const get = Effect.fn("Pty.get")(function* (id: PtyID) {
return (yield* requireSession(id)).info
})
@@ -309,7 +314,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
}
})
return Service.of({ list, get, create, update, remove, write, attach })
return Service.of({ shells, list, get, create, update, remove, write, attach })
}),
)
+6
View File
@@ -226,6 +226,7 @@ export interface Interface {
model: ModelV2.Ref
}) => Effect.Effect<void, NotFoundError>
readonly rename: (input: { sessionID: SessionSchema.ID; title: string }) => Effect.Effect<void, NotFoundError>
readonly archive: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
readonly move: (input: {
sessionID: SessionSchema.ID
directory: AbsolutePath
@@ -705,6 +706,11 @@ const layer = Layer.effect(
title: input.title,
})
}),
archive: Effect.fn("V2Session.archive")(function* (sessionID) {
const session = yield* result.get(sessionID)
if (session.time.archived) return
yield* events.publish(SessionEvent.Archived, { sessionID })
}),
move: Effect.fn("V2Session.move")(function* (input) {
const current = yield* result.get(input.sessionID)
const value = input.directory.trim()
+1 -1
View File
@@ -53,7 +53,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
time: {
created: DateTime.makeUnsafe(row.time_created),
updated: DateTime.makeUnsafe(row.time_updated),
archived: row.time_archived ? DateTime.makeUnsafe(row.time_archived) : undefined,
archived: row.time_archived !== null ? DateTime.makeUnsafe(row.time_archived) : undefined,
},
})
}
@@ -171,6 +171,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
},
"session.moved": () => Effect.void,
"session.renamed": () => Effect.void,
"session.archived": () => Effect.void,
"session.deleted": () => Effect.void,
"session.forked": () => Effect.void,
"session.input.promoted": () => Effect.void,
+11
View File
@@ -609,6 +609,17 @@ const layer = Layer.effectDiscard(
.run()
.pipe(Effect.orDie),
)
yield* events.project(SessionEvent.Archived, (event) =>
db
.update(SessionTable)
.set({
time_archived: DateTime.toEpochMillis(event.created),
time_updated: DateTime.toEpochMillis(event.created),
})
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie),
)
yield* events.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data))
yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event))
yield* events.project(SessionEvent.InputPromoted, (event) =>
+21 -323
View File
@@ -2,30 +2,16 @@ export * as SessionRunnerModel from "./model"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Model } from "@opencode-ai/ai"
// ast-grep-ignore: no-star-import
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
// ast-grep-ignore: no-star-import
import * as OpenAICompatibleChat from "@opencode-ai/ai/protocols/openai-compatible-chat"
// ast-grep-ignore: no-star-import
import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses"
import { Auth, type AnyRoute } from "@opencode-ai/ai/route"
import { Context, Effect, Layer, Schema } from "effect"
import { produce } from "immer"
import { AISDK } from "../../aisdk"
import { Catalog } from "../../catalog"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { ModelResolver } from "../../model-resolver"
import { ModelV2 } from "../../model"
import { Npm } from "@opencode-ai/util/npm"
import { OpenAICodex } from "../../plugin/provider/openai-codex"
import { ProviderV2 } from "../../provider"
import { SessionSchema } from "../schema"
export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelectedError>()(
"SessionRunnerModel.ModelNotSelectedError",
{
sessionID: SessionSchema.ID,
},
{ sessionID: SessionSchema.ID },
) {
override get message() {
return `No model is available for session ${this.sessionID}`
@@ -34,59 +20,19 @@ export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelec
export class ModelUnavailableError extends Schema.TaggedErrorClass<ModelUnavailableError>()(
"SessionRunnerModel.ModelUnavailableError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
},
{ providerID: ProviderV2.ID, modelID: ModelV2.ID },
) {
override get message() {
return `Model unavailable: ${this.providerID}/${this.modelID}`
}
}
export const VariantUnavailableError = ModelResolver.VariantUnavailableError
export type VariantUnavailableError = ModelResolver.VariantUnavailableError
export const UnsupportedPackageError = ModelResolver.UnsupportedPackageError
export type UnsupportedPackageError = ModelResolver.UnsupportedPackageError
export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()(
"SessionRunnerModel.VariantUnavailableError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
variant: ModelV2.VariantID,
},
) {
override get message() {
return `Variant unavailable for ${this.providerID}/${this.modelID}: ${this.variant}`
}
}
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
"SessionRunnerModel.UnsupportedPackageError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
package: Schema.String,
},
) {
override get message() {
return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}`
}
}
export type Error =
| ModelNotSelectedError
| ModelUnavailableError
| VariantUnavailableError
| UnsupportedPackageError
| Integration.AuthorizationError
export interface Resolved {
/** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */
readonly model: Model
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
readonly ref: ModelV2.Ref
/** Catalog capabilities used to shape requests before provider lowering. */
readonly capabilities: ModelV2.Capabilities
/** Catalog pricing in dollars per million tokens. */
readonly cost: ModelV2.Info["cost"]
}
export type Error = ModelNotSelectedError | ModelUnavailableError | ModelResolver.Error
export type Resolved = ModelResolver.Resolved
export interface Interface {
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Resolved, Error>
@@ -94,9 +40,6 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRunnerModel") {}
/** Test or embedding seam for supplying a model resolver directly. */
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
export const resolved = (
model: Model,
@@ -116,276 +59,31 @@ export const resolved = (
cost: options.cost,
})
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
if (credential?.type === "key") return Auth.value(credential.key)
if (credential?.type === "oauth") return Auth.value(credential.access)
const value = model.settings?.apiKey
if (typeof value === "string") return Auth.value(value)
}
const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
route.with({
provider: model.providerID,
endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
headers: providerHeaders(model),
providerOptions: providerOptions(model),
http: model.body === undefined ? undefined : { body: model.body },
limits: { context: model.limit.context, output: model.limit.output },
})
const providerHeaders = (model: ModelV2.Info) => {
const packageName = ProviderV2.packageName(model.package)
const generated = new Map<string, string>()
if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string")
generated.set("OpenAI-Organization", model.settings.organization)
if (packageName === "@ai-sdk/openai" && typeof model.settings?.project === "string")
generated.set("OpenAI-Project", model.settings.project)
if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string")
generated.set("Authorization", `Bearer ${model.settings.authToken}`)
return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
}
const providerOptions = (
model: ModelV2.Info,
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined
const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings
if (Object.keys(settings).length === 0) return undefined
const packageName = ProviderV2.packageName(model.package)
if (packageName === "@ai-sdk/openai") return { openai: settings }
if (packageName === "@ai-sdk/anthropic") return { anthropic: settings }
if (packageName === "@ai-sdk/openai-compatible") return { openai: settings }
}
export const withVariant = (
model: ModelV2.Info,
variantID: ModelV2.VariantID | undefined,
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
const id = variantID === "default" ? undefined : variantID
const variant = model.variants?.find((item) => item.id === id)
if (!variant && variantID !== undefined && variantID !== "default")
return Effect.fail(
new VariantUnavailableError({
providerID: model.providerID,
modelID: model.id,
variant: variantID,
}),
)
return Effect.succeed(
variant
? produce(model, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings)
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
})
: model,
)
}
export interface Dependencies {
readonly loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError>
readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect<Model, AISDK.InitError>
}
export const fromCatalogModel = (
model: ModelV2.Info,
credential?: Credential.Value,
dependencies: Dependencies = {},
): Effect.Effect<Model, UnsupportedPackageError> => {
const resolved = produce(model, (draft) => {
if (draft.settings?.apiKey === "") delete draft.settings.apiKey
if (credential?.type === "key" && credential.metadata !== undefined)
draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata)
})
const packageName = ProviderV2.packageName(resolved.package)
const key = apiKey(resolved, credential)
if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
return Effect.succeed(codexModel(resolved, credential, key))
}
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
return Effect.succeed(
withDefaults(resolved, OpenAIResponses.route)
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
return Effect.succeed(
withDefaults(resolved, AnthropicMessages.route)
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (
ProviderV2.isAISDK(resolved.package) &&
packageName === "@ai-sdk/openai-compatible" &&
typeof resolved.settings?.baseURL === "string"
) {
return Effect.succeed(
withDefaults(resolved, OpenAICompatibleChat.route)
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (ProviderV2.isAISDK(resolved.package)) {
if (!dependencies.loadAISDK) return Effect.fail(unsupported(resolved))
const runtime = produce(resolved, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, {
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
...credential?.metadata,
})
})
return dependencies.loadAISDK(runtime).pipe(Effect.mapError(() => unsupported(resolved)))
}
if (!resolved.package) return Effect.fail(unsupported(resolved))
const specifier = resolved.package
return Effect.gen(function* () {
const module = yield* (dependencies.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe(
Effect.mapError(() => unsupported(resolved)),
)
const configured = { ...resolved.settings, ...credential?.metadata }
const settings = {
...(credential ? withoutNativeAuthSettings(configured) : configured),
...nativeCredentialSettings(specifier, credential),
headers: resolved.headers,
body: resolved.body,
limits: { context: resolved.limit.context, output: resolved.limit.output },
}
return yield* Effect.try({
try: () => {
const runtime = module.model(resolved.modelID ?? resolved.id, settings)
return Model.update(runtime, {
provider: resolved.providerID,
compatibility: resolved.compatibility
? { ...runtime.compatibility, ...resolved.compatibility }
: runtime.compatibility,
})
},
catch: () => unsupported(resolved),
})
})
}
const isNativeOpenAI = (packageName: string | undefined) =>
packageName === "@opencode-ai/ai/providers/openai" ||
packageName?.startsWith("@opencode-ai/ai/providers/openai/") === true
const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => {
if (!credential) return {}
if (credential.type === "key") return { apiKey: credential.key }
if (
specifier === "@opencode-ai/ai/providers/anthropic" ||
specifier === "@opencode-ai/ai/providers/anthropic-compatible"
)
return { authToken: credential.access }
if (
specifier === "@opencode-ai/ai/providers/google-vertex" ||
specifier.startsWith("@opencode-ai/ai/providers/google-vertex/")
)
return { accessToken: credential.access }
return { apiKey: credential.access }
}
const withoutNativeAuthSettings = (settings: Record<string, unknown>) => {
const { accessToken: _accessToken, apiKey: _apiKey, authToken: _authToken, ...rest } = settings
return rest
}
const codexModel = (
model: ModelV2.Info,
credential: Credential.Value | undefined,
key: ReturnType<typeof Auth.value> | undefined,
) => {
const account = OpenAICodex.accountID(credential)
return withDefaults(model, OpenAIResponses.route)
.with({
endpoint: { baseURL: OpenAICodex.baseURL },
auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen(
account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }),
),
})
.model({ id: model.modelID ?? model.id, compatibility: model.compatibility })
}
const unsupported = (model: ModelV2.Info) =>
new UnsupportedPackageError({
providerID: model.providerID,
modelID: model.id,
package: model.package ?? "unknown",
})
export const resolve = (
session: SessionSchema.Info,
model: ModelV2.Info,
credential?: Credential.Value,
dependencies?: Dependencies,
) =>
withVariant(model, session.model?.variant).pipe(
Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)),
)
export const supported = (model: ModelV2.Info) => Boolean(model.package)
/** Resolves models from the catalog belonging to the current Location runtime. */
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
const npm = yield* Npm.Service
const aisdk = yield* AISDK.Service
const resolver = yield* ModelResolver.Service
return Service.of({
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
// Location plugins populate and filter the catalog asynchronously during layer startup.
const defaultModel = session.model ? undefined : yield* catalog.model.default()
const selected = session.model
? (yield* catalog.model.available()).find(
(model) => model.providerID === session.model?.providerID && model.id === session.model.id,
)
: defaultModel && supported(defaultModel)
? defaultModel
: (yield* catalog.model.available()).find(supported)
if (!selected && session.model)
if (!session.model) {
const resolved = yield* resolver.resolve()
if (resolved) return resolved
return yield* new ModelNotSelectedError({ sessionID: session.id })
}
const selected = (yield* catalog.model.available()).find(
(model) => model.providerID === session.model?.providerID && model.id === session.model.id,
)
if (!selected)
return yield* new ModelUnavailableError({
providerID: session.model.providerID,
modelID: session.model.id,
})
if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id })
const provider = yield* catalog.provider.get(selected.providerID)
const connection = yield* integrations.connection.active(
provider?.integrationID ?? Integration.ID.make(selected.providerID),
)
const model = yield* resolve(
session,
selected,
connection ? yield* integrations.connection.resolve(connection) : undefined,
{
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
loadAISDK: (model) => aisdk.model(model),
},
)
return {
model,
ref: ModelV2.Ref.make({
id: selected.id,
providerID: selected.providerID,
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
}),
capabilities: selected.capabilities,
cost: selected.cost,
}
return yield* resolver.resolveModel(selected, session.model.variant)
}),
})
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [Catalog.node, Integration.node, Npm.node, AISDK.node],
})
export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, ModelResolver.node] })
+1 -1
View File
@@ -161,7 +161,7 @@ export function migrateAgent(info: ConfigAgentV1.Info) {
description: info.description,
mode: info.mode,
hidden: info.hidden,
color: info.color,
color: info.color === undefined ? undefined : info.color.startsWith("#") ? info.color : "#aaaaaa",
steps: info.steps,
disabled: info.disable,
permissions: permissions(info.permission),
+7 -2
View File
@@ -2,7 +2,7 @@ export * as Vcs from "./vcs"
import { Context, Effect, Layer } from "effect"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { FileStatus, Mode } from "@opencode-ai/schema/vcs"
import { FileStatus, Info, Mode } from "@opencode-ai/schema/vcs"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Location } from "./location"
@@ -10,13 +10,14 @@ import { AppProcess } from "@opencode-ai/util/process"
import { VcsGit } from "./vcs/git"
import { VcsHg } from "./vcs/hg"
export { FileStatus, Mode }
export { FileStatus, Info, Mode }
export interface DiffOptions {
readonly context?: number
}
export interface Interface {
readonly info: () => Effect.Effect<Info>
readonly status: () => Effect.Effect<FileStatus[]>
readonly diff: (mode: Mode, options?: DiffOptions) => Effect.Effect<FileDiff.Info[]>
}
@@ -40,6 +41,10 @@ const layer = Layer.effect(
const location = yield* Location.Service
const impl = adapter(proc, fs, location)
return Service.of({
info: Effect.fn("Vcs.info")(function* () {
if (!impl) return {}
return yield* impl.info()
}),
status: Effect.fn("Vcs.status")(function* () {
if (!impl) return []
return yield* impl.status()
+6
View File
@@ -20,6 +20,12 @@ export function make(proc: AppProcess.Interface, input: { directory: string; wor
const ctx: Ctx = { git: makeGit(proc), directory: input.directory, worktree: input.worktree }
return {
info: Effect.fn("VcsGit.info")(function* () {
const [branch, root] = yield* Effect.all([ctx.git.branch(ctx.directory), ctx.git.defaultBranch(ctx.directory)], {
concurrency: 2,
})
return { branch, defaultBranch: root?.name }
}),
status: Effect.fn("VcsGit.status")(function* () {
const git = ctx.git
const ref = (yield* git.hasHead(ctx.directory)) ? "HEAD" : undefined
+3
View File
@@ -73,6 +73,9 @@ export function make(
})
return {
info: Effect.fn("VcsHg.info")(function* () {
return { branch: yield* hg.branch(), defaultBranch: "default" }
}),
status: Effect.fn("VcsHg.status")(function* () {
const [items, batch] = yield* Effect.all(
// Zero-context patches are enough to count changed lines.
+1
View File
@@ -59,6 +59,7 @@ describe("CommandV2", () => {
providerID: ProviderV2.ID.make("anthropic"),
variant: ModelV2.VariantID.make("high"),
},
source: "command",
}),
])
}),
+7 -3
View File
@@ -1,4 +1,4 @@
import { describe, expect } from "bun:test"
import { describe, expect, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Effect, Schema } from "effect"
@@ -23,6 +23,10 @@ const defaultPermissions = [
{ action: "external_directory", resource: "*", effect: "ask" },
] satisfies PermissionV2.Ruleset
test("rejects named agent color tokens", () => {
expect(() => decode({ agents: { reviewer: { color: "warning" } } })).toThrow()
})
describe("ConfigAgentPlugin.Plugin", () => {
it.effect("matches POSIX paths against home-relative permissions", () =>
Effect.gen(function* () {
@@ -160,7 +164,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
description: "Reviews changes",
mode: "subagent",
hidden: true,
color: "warning",
color: "#ff6b6b",
steps: 12,
request: {
headers: { first: "one", shared: "first" },
@@ -197,7 +201,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
description: "Reviews changes",
mode: "subagent",
hidden: true,
color: "warning",
color: "#ff6b6b",
steps: 12,
model: { providerID: "anthropic", id: "claude-sonnet" },
})
+3 -2
View File
@@ -95,9 +95,10 @@ Review files`,
variant: ModelV2.VariantID.make("high"),
},
subtask: true,
source: "command",
}),
CommandV2.Info.make({ name: "empty", template: "" }),
CommandV2.Info.make({ name: "nested/docs", template: "Write docs" }),
CommandV2.Info.make({ name: "empty", template: "", source: "command" }),
CommandV2.Info.make({ name: "nested/docs", template: "Write docs", source: "command" }),
])
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "commands", "review.md"), "Review again"))
+2 -2
View File
@@ -738,7 +738,7 @@ describe("Config", () => {
system: "Find regressions.",
mode: "subagent",
hidden: false,
color: "warning",
color: "#ff6b6b",
steps: 12,
disabled: false,
permissions: [{ action: "edit", resource: "*", effect: "deny" }],
@@ -824,7 +824,7 @@ describe("Config", () => {
expect(reviewer?.system).toBe("Find regressions.")
expect(reviewer?.mode).toBe("subagent")
expect(reviewer?.hidden).toBe(false)
expect(reviewer?.color).toBe("warning")
expect(reviewer?.color).toBe("#ff6b6b")
expect(reviewer?.steps).toBe(12)
expect(reviewer?.disabled).toBe(false)
expect(reviewer?.permissions).toEqual([{ action: "edit", resource: "*", effect: "deny" }])
@@ -79,6 +79,7 @@ describe("node build", () => {
return Project.Service.of({
list: () => Effect.succeed([]),
directories: () => Effect.succeed([]),
update: () => Effect.die("unused"),
resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }),
commit: () => Effect.void,
})
+111
View File
@@ -0,0 +1,111 @@
import { expect } from "bun:test"
import { LLMClient, LLMEvent, LLMResponse, Model } from "@opencode-ai/ai"
import { OpenAIChat } from "@opencode-ai/ai/protocols"
import { AISDK } from "@opencode-ai/core/aisdk"
import { Catalog } from "@opencode-ai/core/catalog"
import { Generate } from "@opencode-ai/core/generate"
import { Integration } from "@opencode-ai/core/integration"
import { ModelResolver } from "@opencode-ai/core/model-resolver"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Npm } from "@opencode-ai/util/npm"
import { Effect, Layer, Stream } from "effect"
import { testEffect } from "./lib/effect"
const selected = ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("gemini")),
package: ProviderV2.aisdk("@ai-sdk/google"),
})
const runtime = Model.make({ id: "gemini", provider: "test-provider", route: OpenAIChat.route })
const catalog = Layer.mock(Catalog.Service, {
provider: {
get: () => Effect.succeed(undefined),
all: () => Effect.die("unused"),
available: () => Effect.die("unused"),
},
model: {
get: () => Effect.succeed(selected),
all: () => Effect.die("unused"),
available: () => Effect.die("unused"),
default: () => Effect.die("unused"),
small: () => Effect.die("unused"),
},
})
const integrations = Layer.mock(Integration.Service, {
connection: {
active: () => Effect.succeed(undefined),
resolve: () => Effect.die("unused"),
key: () => Effect.die("unused"),
update: () => Effect.die("unused"),
remove: () => Effect.die("unused"),
},
oauth: {
connect: () => Effect.die("unused"),
status: () => Effect.die("unused"),
complete: () => Effect.die("unused"),
cancel: () => Effect.die("unused"),
},
command: {
connect: () => Effect.die("unused"),
status: () => Effect.die("unused"),
cancel: () => Effect.die("unused"),
},
})
const npm = Layer.mock(Npm.Service, {
add: () => Effect.die("unused"),
install: () => Effect.die("unused"),
which: () => Effect.die("unused"),
})
const aisdk = Layer.mock(AISDK.Service, {
hook: {
sdk: () => Effect.die("unused"),
language: () => Effect.die("unused"),
},
model: () => Effect.succeed(runtime),
})
const client = Layer.mock(LLMClient.Service)({
prepare: () => Effect.die("unused"),
stream: () => Stream.die("unused"),
generate: () =>
Effect.sync(() => {
const response = LLMResponse.fromEvents([
LLMEvent.textStart({ id: "generate" }),
LLMEvent.textDelta({ id: "generate", text: "OK" }),
LLMEvent.textEnd({ id: "generate" }),
LLMEvent.finish({ reason: "stop" }),
])
if (!response) throw new Error("Incomplete generate response")
return response
}),
})
const resolver = ModelResolver.layer.pipe(Layer.provide(Layer.mergeAll(catalog, integrations, npm, aisdk)))
const it = testEffect(Generate.layer.pipe(Layer.provide(Layer.merge(resolver, client))))
const resolverIt = testEffect(resolver)
it.effect("loads dynamic AI SDK models", () =>
Effect.gen(function* () {
const generate = yield* Generate.Service
const result = yield* generate.text({
prompt: "Return exactly OK",
model: ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }),
})
expect(result).toBe("OK")
}),
)
resolverIt.effect("resolves dynamic models with their catalog metadata", () =>
Effect.gen(function* () {
const resolver = yield* ModelResolver.Service
const result = yield* resolver.resolve(ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }))
expect(result).toEqual({
model: runtime,
ref: ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }),
capabilities: selected.capabilities,
cost: selected.cost,
})
}),
)
+1
View File
@@ -14,6 +14,7 @@ const projectLayer = Layer.succeed(
Project.Service.of({
list: () => Effect.succeed([]),
directories: () => Effect.succeed([]),
update: () => Effect.die("unused"),
resolve: () =>
Effect.succeed({
id: Project.ID.make("project"),
@@ -1,17 +1,13 @@
import { describe, expect } from "bun:test"
import { LLM, Model } from "@opencode-ai/ai"
import { LLMClient } from "@opencode-ai/ai/route"
import { DateTime, Effect } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { Effect } from "effect"
import { Headers } from "effect/unstable/http"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ProjectV2 } from "@opencode-ai/core/project"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { SessionV2 } from "@opencode-ai/core/session"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { ModelResolver } from "@opencode-ai/core/model-resolver"
import { it } from "./lib/effect"
interface ModelOptions {
@@ -43,13 +39,13 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) =>
limit: { context: 100, output: 20 },
})
describe("SessionRunnerModel", () => {
describe("ModelResolver", () => {
it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () =>
Effect.gen(function* () {
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
})
const resolved = yield* SessionRunnerModel.fromCatalogModel(catalog)
const resolved = yield* ModelResolver.fromCatalogModel(catalog)
expect(catalog.id).toBe(ModelV2.ID.make("test-model"))
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
@@ -68,7 +64,7 @@ describe("SessionRunnerModel", () => {
it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { apiKey: "secret", baseURL: "https://openai.example/v1" },
}),
@@ -82,7 +78,7 @@ describe("SessionRunnerModel", () => {
it.effect("treats an empty configured API key as omitted", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { apiKey: "", baseURL: "https://openai.example/v1" },
}),
@@ -101,7 +97,7 @@ describe("SessionRunnerModel", () => {
it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
compatibility: { reasoningField: "vendor_reasoning" },
settings: {
@@ -130,7 +126,7 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("overlays selected OpenAI Session variant settings and bodies", () =>
it.effect("overlays selected OpenAI variant settings and bodies", () =>
Effect.gen(function* () {
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
@@ -147,22 +143,7 @@ describe("SessionRunnerModel", () => {
},
],
})
const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_model_variant"),
projectID: ProjectV2.ID.global,
title: "test",
model: {
id: catalog.id,
providerID: catalog.providerID,
variant: ModelV2.VariantID.make("high"),
},
cost: Money.USD.zero,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: { directory: AbsolutePath.make("/project") },
})
const resolved = yield* SessionRunnerModel.resolve(session, catalog)
const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high"))
expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" })
expect(resolved.route.defaults.http?.body).toEqual({
@@ -177,7 +158,7 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("overlays selected OpenAI-compatible Session variant bodies", () =>
it.effect("overlays selected OpenAI-compatible variant bodies", () =>
Effect.gen(function* () {
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
settings: { baseURL: "https://compatible.example/v1" },
@@ -190,18 +171,7 @@ describe("SessionRunnerModel", () => {
},
],
})
const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_compatible_variant"),
projectID: ProjectV2.ID.global,
title: "test",
model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") },
cost: Money.USD.zero,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: { directory: AbsolutePath.make("/project") },
})
const resolved = yield* SessionRunnerModel.resolve(session, catalog)
const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high"))
expect(resolved.route.defaults.http?.body).toEqual({
custom_extension: { enabled: true },
@@ -211,27 +181,12 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
it.effect("rejects an explicit unavailable variant during model resolution", () =>
Effect.gen(function* () {
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
})
const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_model_variant_unavailable"),
projectID: ProjectV2.ID.global,
title: "test",
model: {
id: catalog.id,
providerID: catalog.providerID,
variant: ModelV2.VariantID.make("unknown"),
},
cost: Money.USD.zero,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: { directory: AbsolutePath.make("/project") },
})
const failure = yield* SessionRunnerModel.resolve(session, catalog).pipe(Effect.flip)
const failure = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("unknown")).pipe(Effect.flip)
expect(failure).toMatchObject({
_tag: "SessionRunnerModel.VariantUnavailableError",
@@ -243,7 +198,7 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("overlays selected Anthropic Session variant settings", () =>
it.effect("overlays selected Anthropic variant settings", () =>
Effect.gen(function* () {
const catalog = model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
settings: { baseURL: "https://anthropic.example/v1" },
@@ -256,18 +211,7 @@ describe("SessionRunnerModel", () => {
},
],
})
const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_anthropic_variant"),
projectID: ProjectV2.ID.global,
title: "test",
model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") },
cost: Money.USD.zero,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: { directory: AbsolutePath.make("/project") },
})
const resolved = yield* SessionRunnerModel.resolve(session, catalog)
const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high"))
expect(resolved.route.defaults.http?.body).toEqual({
custom_extension: { enabled: true },
@@ -280,7 +224,7 @@ describe("SessionRunnerModel", () => {
it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
settings: { baseURL: "https://anthropic.example/v1" },
}),
@@ -296,7 +240,7 @@ describe("SessionRunnerModel", () => {
it.effect("uses resolved credentials for bearer auth", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
headers: {},
@@ -320,7 +264,7 @@ describe("SessionRunnerModel", () => {
it.effect("prefers stored credentials over configured auth", () =>
Effect.gen(function* () {
const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" },
headers: {},
@@ -343,7 +287,7 @@ describe("SessionRunnerModel", () => {
it.effect("does not project OAuth account metadata into the request body", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
headers: {},
@@ -365,7 +309,7 @@ describe("SessionRunnerModel", () => {
it.effect("routes ChatGPT OAuth credentials to the codex backend", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
headers: {},
@@ -400,7 +344,7 @@ describe("SessionRunnerModel", () => {
it.effect("routes native OpenAI provider packages with ChatGPT credentials to the codex backend", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model("@opencode-ai/ai/providers/openai", {
settings: { baseURL: "https://openai.example/v1" },
}),
@@ -429,7 +373,7 @@ describe("SessionRunnerModel", () => {
it.effect("does not route native OpenAI-compatible packages to the codex backend", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model("@opencode-ai/ai/providers/openai-compatible", {
settings: { baseURL: "https://compatible.example/v1" },
}),
@@ -450,7 +394,7 @@ describe("SessionRunnerModel", () => {
it.effect("maps legacy OpenAI organization and project settings to headers", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { organization: "org_123", project: "proj_123" },
}),
@@ -465,7 +409,7 @@ describe("SessionRunnerModel", () => {
it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
headers: {},
@@ -496,7 +440,7 @@ describe("SessionRunnerModel", () => {
it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
headers: {},
@@ -528,12 +472,12 @@ describe("SessionRunnerModel", () => {
it.effect("loads dynamic native provider packages through the injected package loader", () =>
Effect.gen(function* () {
const native = yield* SessionRunnerModel.fromCatalogModel(
const native = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
}),
)
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model("@opencode-ai/ai/providers/custom", {
settings: { region: "test" },
headers: { "x-package": "header" },
@@ -565,7 +509,7 @@ describe("SessionRunnerModel", () => {
it.effect("maps OAuth credentials to native provider auth settings", () =>
Effect.gen(function* () {
const native = yield* SessionRunnerModel.fromCatalogModel(
const native = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
}),
@@ -588,7 +532,7 @@ describe("SessionRunnerModel", () => {
] as const
yield* Effect.forEach(packages, ([specifier, key]) =>
SessionRunnerModel.fromCatalogModel(model(specifier, { settings: { apiKey: "configured-key" } }), credential, {
ModelResolver.fromCatalogModel(model(specifier, { settings: { apiKey: "configured-key" } }), credential, {
loadPackage: () =>
Effect.succeed({
model: (modelID, settings) => {
@@ -604,12 +548,12 @@ describe("SessionRunnerModel", () => {
it.effect("loads arbitrary AISDK packages through the injected AISDK loader", () =>
Effect.gen(function* () {
const native = yield* SessionRunnerModel.fromCatalogModel(
const native = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
}),
)
const resolved = yield* SessionRunnerModel.fromCatalogModel(
const resolved = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/google"), {
modelID: "gemini-api-model",
settings: { project: "test" },
@@ -644,7 +588,7 @@ describe("SessionRunnerModel", () => {
it.effect("rejects AISDK packages without an available loader", () =>
Effect.gen(function* () {
const failure = yield* SessionRunnerModel.fromCatalogModel(
const failure = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/google"), {
settings: { baseURL: "https://google.example/v1" },
}),
@@ -662,12 +606,12 @@ describe("SessionRunnerModel", () => {
it.effect("drops an empty API key before loading an AISDK package", () =>
Effect.gen(function* () {
const native = yield* SessionRunnerModel.fromCatalogModel(
const native = yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
}),
)
yield* SessionRunnerModel.fromCatalogModel(
yield* ModelResolver.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/google"), {
settings: { apiKey: "", baseURL: "https://google.example/v1" },
}),
@@ -685,9 +629,9 @@ describe("SessionRunnerModel", () => {
it.effect("reports whether a catalog model declares a provider package", () =>
Effect.sync(() => {
expect(SessionRunnerModel.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true)
expect(SessionRunnerModel.supported(model("@opencode-ai/ai/providers/custom"))).toBe(true)
expect(SessionRunnerModel.supported(model(undefined))).toBe(false)
expect(ModelResolver.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true)
expect(ModelResolver.supported(model("@opencode-ai/ai/providers/custom"))).toBe(true)
expect(ModelResolver.supported(model(undefined))).toBe(false)
}),
)
})
@@ -1,96 +0,0 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test"
import { createAlibaba } from "@ai-sdk/alibaba"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* AlibabaPlugin.effect(host)
})
describe("AlibabaPlugin", () => {
it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
modelID: ModelV2.ID.make("qwen"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/alibaba",
options: { name: "alibaba" },
})
expect(result.sdk).toBeDefined()
}),
)
it.effect("ignores non-Alibaba SDK packages", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
modelID: ModelV2.ID.make("qwen"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "alibaba" },
})
expect(result.sdk).toBeUndefined()
}),
)
it.effect("matches the old bundled Alibaba SDK provider naming", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
modelID: ModelV2.ID.make("qwen"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/alibaba",
options: { name: "custom-alibaba", apiKey: "test" },
})
const expected = createAlibaba({ apiKey: "test", ...{ name: "custom-alibaba" } }).languageModel("qwen")
const actual = result.sdk?.languageModel("qwen")
expect(actual?.provider).toBe(expected.provider)
expect(actual?.modelId).toBe(expected.modelId)
}),
)
it.effect("uses the default languageModel(modelID) behavior", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const item = ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("qwen-plus"),
package: "aisdk:test-provider",
})
const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} })
const language = result.sdk?.languageModel(item.modelID ?? item.id)
expect(language?.modelId).toBe("qwen-plus")
expect(language?.provider).toBe("alibaba.chat")
}),
)
})
@@ -1,127 +0,0 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere"
import { ProviderV2 } from "@opencode-ai/core/provider"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const cohereOptions: Record<string, any>[] = []
const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* CoherePlugin.effect(host)
})
function fakeSelectorSdk(calls: string[]) {
const make = (method: string) => (id: string) => {
calls.push(`${method}:${id}`)
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
}
return {
responses: make("responses"),
messages: make("messages"),
chat: make("chat"),
languageModel: make("languageModel"),
}
}
void mock.module("@ai-sdk/cohere", () => ({
createCohere: (options: Record<string, any>) => {
cohereOptions.push({ ...options })
return {
languageModel: (modelID: string) => ({
modelID,
provider: `${options.name ?? "cohere"}.chat`,
specificationVersion: "v3",
}),
}
},
}))
describe("CoherePlugin", () => {
it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
modelID: ModelV2.ID.make("command"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cohere" },
})
expect(ignored.sdk).toBeUndefined()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
modelID: ModelV2.ID.make("command"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/cohere",
options: { name: "cohere" },
})
expect(result.sdk).toBeDefined()
}),
)
it.effect("uses the model provider ID as the bundled SDK name", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")),
modelID: ModelV2.ID.make("command-r-plus"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/cohere",
options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
})
expect(cohereOptions.at(-1)).toEqual({
name: "custom-cohere",
apiKey: "test",
baseURL: "https://cohere.example",
})
expect(result.sdk?.languageModel("command-r-plus").provider).toBe("custom-cohere.chat")
}),
)
it.effect("leaves language selection to the default languageModel fallback", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = []
const sdk = fakeSelectorSdk(calls)
yield* addPlugin()
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("command-r-plus"),
package: "aisdk:test-provider",
}),
sdk,
options: {},
})
expect(result.language).toBeUndefined()
expect(calls).toEqual([])
expect(result.language ?? sdk.languageModel("command-r-plus")).toBeDefined()
expect(calls).toEqual(["languageModel:command-r-plus"])
}),
)
})
@@ -1,161 +0,0 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const deepinfraOptions: Record<string, unknown>[] = []
const deepinfraLanguageModels: string[] = []
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* DeepInfraPlugin.effect(host)
})
void mock.module("@ai-sdk/deepinfra", () => ({
createDeepInfra: (options: Record<string, unknown>) => {
const captured = { ...options }
deepinfraOptions.push(captured)
return {
languageModel: (modelID: string) => {
deepinfraLanguageModels.push(modelID)
return { modelID, provider: `${captured.name ?? "deepinfra"}.chat`, specificationVersion: "v3" }
},
}
},
}))
function resetDeepInfraMock() {
deepinfraOptions.length = 0
deepinfraLanguageModels.length = 0
}
describe("DeepInfraPlugin", () => {
it.effect("creates a DeepInfra SDK for @ai-sdk/deepinfra", () =>
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" },
})
expect(result.sdk).toBeDefined()
}),
)
it.effect("passes the model provider ID as the bundled DeepInfra SDK name", () =>
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
}),
package: "@ai-sdk/deepinfra",
options: { name: "custom-deepinfra", apiKey: "test" },
})
expect(result.sdk.languageModel("model").provider).toBe("custom-deepinfra.chat")
expect(deepinfraOptions).toEqual([{ name: "custom-deepinfra", apiKey: "test" }])
}),
)
it.effect("uses the canonical provider ID as the bundled DeepInfra SDK name", () =>
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra", apiKey: "test" },
})
expect(result.sdk.languageModel("model").provider).toBe("deepinfra.chat")
expect(deepinfraOptions).toEqual([{ name: "deepinfra", apiKey: "test" }])
}),
)
it.effect("matches only the exact bundled DeepInfra package", () =>
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const packages = [
"unmatched-package",
"@ai-sdk/deepinfra-compatible",
"file:///tmp/@ai-sdk/deepinfra-provider.js",
]
yield* Effect.forEach(packages, (item) =>
Effect.gen(function* () {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
}),
package: item,
options: { name: "deepinfra" },
})
expect(ignored.sdk).toBeUndefined()
}),
)
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" },
})
expect(result.sdk).toBeDefined()
expect(deepinfraOptions).toEqual([{ name: "deepinfra" }])
}),
)
it.effect("uses the default languageModel selection for DeepInfra models", () =>
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const sdkEvent = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")),
modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
package: "aisdk:@ai-sdk/deepinfra",
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" },
})
const result = yield* aisdk.runLanguage({ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options })
const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id)
expect(language.provider).toBe("deepinfra.chat")
expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
}),
)
})
@@ -0,0 +1,60 @@
import { expect } from "bun:test"
import { Effect } from "effect"
import { AISDK } from "@opencode-ai/core/aisdk"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere"
import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra"
import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway"
import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq"
import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral"
import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity"
import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai"
import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const modelID = ModelV2.ID.make("test-model")
const options = { name: "custom-provider", apiKey: "test", baseURL: "https://example.test" }
const providers = [
{ id: "alibaba", plugin: AlibabaPlugin, package: "@ai-sdk/alibaba", provider: "alibaba.chat" },
{ id: "cohere", plugin: CoherePlugin, package: "@ai-sdk/cohere", provider: "cohere.chat" },
{ id: "deepinfra", plugin: DeepInfraPlugin, package: "@ai-sdk/deepinfra", provider: "deepinfra.chat" },
{ id: "gateway", plugin: GatewayPlugin, package: "@ai-sdk/gateway", provider: "gateway" },
{ id: "groq", plugin: GroqPlugin, package: "@ai-sdk/groq", provider: "groq.chat" },
{ id: "mistral", plugin: MistralPlugin, package: "@ai-sdk/mistral", provider: "mistral.chat" },
{ id: "perplexity", plugin: PerplexityPlugin, package: "@ai-sdk/perplexity", provider: "perplexity" },
{ id: "togetherai", plugin: TogetherAIPlugin, package: "@ai-sdk/togetherai", provider: "togetherai.chat" },
{ id: "venice", plugin: VenicePlugin, package: "venice-ai-sdk-provider", provider: "custom-provider.chat" },
] as const
const it = testEffect(PluginTestLayer)
providers.forEach((item) =>
it.effect(`${item.id} loads only its exact package`, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* item.plugin.effect(host)
const model = ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make(item.id), modelID),
modelID,
package: ProviderV2.aisdk(item.package),
})
const matched = yield* aisdk.runSDK({ model, package: item.package, options })
const ignored = yield* aisdk.runSDK({ model, package: `${item.package}/unsupported`, options })
const language = matched.sdk?.languageModel(modelID)
expect({
provider: language?.provider,
modelID: language?.modelId,
version: language?.specificationVersion,
ignored: ignored.sdk === undefined,
}).toEqual({ provider: item.provider, modelID: "test-model", version: "v3", ignored: true })
}),
),
)
@@ -1,115 +0,0 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const gatewayCalls: Record<string, unknown>[] = []
const vercelGatewayModels = ["anthropic/claude-sonnet-4", "openai/gpt-5", "google/gemini-2.5-pro"]
const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* GatewayPlugin.effect(host)
})
mock.module("@ai-sdk/gateway", () => ({
createGateway(options: Record<string, unknown>) {
gatewayCalls.push({ ...options })
return {
languageModel(modelID: string) {
return {
modelId: modelID,
provider: options.name,
specificationVersion: "v3",
}
},
}
},
}))
describe("GatewayPlugin", () => {
it.effect("creates a Gateway SDK for @ai-sdk/gateway", () =>
Effect.gen(function* () {
gatewayCalls.length = 0
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/gateway",
options: { name: "gateway" },
})
expect(result.sdk).toBeDefined()
expect(gatewayCalls).toHaveLength(1)
}),
)
it.effect("passes the model providerID as the Gateway SDK name", () =>
Effect.gen(function* () {
gatewayCalls.length = 0
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")),
modelID: ModelV2.ID.make("anthropic/claude-sonnet-4"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/gateway",
options: { name: "vercel", apiKey: "test-key" },
})
expect(gatewayCalls).toEqual([{ name: "vercel", apiKey: "test-key" }])
expect(result.sdk.languageModel("anthropic/claude-sonnet-4").provider).toBe("vercel")
}),
)
it.effect("matches Vercel AI Gateway models by their @ai-sdk/gateway package", () =>
Effect.gen(function* () {
gatewayCalls.length = 0
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
for (const modelID of vercelGatewayModels) {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
modelID: ModelV2.ID.make(modelID),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/vercel",
options: { name: "vercel" },
})
expect(ignored.sdk).toBeUndefined()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
modelID: ModelV2.ID.make(modelID),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/gateway",
options: { name: "vercel" },
})
expect(result.sdk).toBeDefined()
}
expect(gatewayCalls).toHaveLength(3)
}),
)
})
@@ -1,122 +0,0 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test"
import { createGroq } from "@ai-sdk/groq"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* GroqPlugin.effect(host)
})
describe("GroqPlugin", () => {
it.effect("creates a Groq SDK for @ai-sdk/groq", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
}),
package: "@ai-sdk/groq",
options: { name: "groq" },
})
expect(result.sdk).toBeDefined()
}),
)
it.effect("ignores non-Groq SDK packages", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "groq" },
})
expect(result.sdk).toBeUndefined()
}),
)
it.effect("only matches the bundled @ai-sdk/groq package exactly", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
}),
package: "@ai-sdk/groq/compat",
options: { name: "groq" },
})
expect(result.sdk).toBeUndefined()
}),
)
it.effect("matches the old bundled Groq SDK provider naming", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")),
modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
}),
package: "@ai-sdk/groq",
options: { name: "custom-groq", apiKey: "test" },
})
const expected = createGroq({ name: "custom-groq", apiKey: "test" } as Parameters<typeof createGroq>[0] & {
name: string
}).languageModel("llama")
const actual = result.sdk?.languageModel("llama")
expect(actual?.provider).toBe(expected.provider)
expect(actual?.modelId).toBe(expected.modelId)
}),
)
it.effect("uses the default languageModel(modelID) behavior", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const sdk = createGroq({ name: "groq", apiKey: "test" } as Parameters<typeof createGroq>[0] & {
name: string
})
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("llama-api"),
package: "aisdk:@ai-sdk/groq",
}),
sdk,
options: { name: "groq", apiKey: "test" },
})
const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
expect(language.modelId).toBe("llama-api")
expect(language.provider).toBe("groq.chat")
}),
)
})
@@ -1,134 +0,0 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* MistralPlugin.effect(host)
})
describe("MistralPlugin", () => {
it.effect("creates a Mistral SDK for @ai-sdk/mistral", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/mistral",
options: { name: "mistral" },
})
expect(result.sdk).toBeDefined()
}),
)
it.effect("ignores non-Mistral SDK packages", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "mistral" },
})
expect(result.sdk).toBeUndefined()
}),
)
it.effect("matches the old bundled Mistral SDK provider name for the bundled provider ID", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const providers: string[] = []
yield* addPlugin()
yield* aisdk.hook.sdk((event) =>
Effect.sync(() => {
providers.push(event.sdk.languageModel("mistral-large").provider)
}),
)
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/mistral",
options: { name: "mistral" },
})
expect(result.sdk).toBeDefined()
expect(providers).toEqual(["mistral.chat"])
}),
)
it.effect("matches the old bundled Mistral SDK provider name for custom provider IDs", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const providers: string[] = []
yield* addPlugin()
yield* aisdk.hook.sdk((event) =>
Effect.sync(() => {
providers.push(event.sdk.languageModel("mistral-large").provider)
}),
)
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")),
modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/mistral",
options: { name: "custom-mistral" },
})
expect(providers).toEqual(["mistral.chat"])
}),
)
it.effect("leaves Mistral language selection on the default sdk.languageModel(modelID) path", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = []
const sdk = {
languageModel: (id: string) => {
calls.push(`languageModel:${id}`)
return { modelId: id, provider: "languageModel", specificationVersion: "v3" } as unknown as LanguageModelV3
},
}
yield* addPlugin()
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
}),
sdk,
options: {},
})
const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
expect(calls).toEqual(["languageModel:mistral-large"])
expect(language).toBeDefined()
}),
)
})
@@ -1,127 +0,0 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* PerplexityPlugin.effect(host)
})
function fakeSelectorSdk(calls: string[]) {
const make = (method: string) => (id: string) => {
calls.push(`${method}:${id}`)
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
}
return {
responses: make("responses"),
messages: make("messages"),
chat: make("chat"),
languageModel: make("languageModel"),
}
}
describe("PerplexityPlugin", () => {
it.effect("creates a Perplexity SDK for the exact @ai-sdk/perplexity package", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/perplexity",
options: { name: "perplexity" },
})
expect(result.sdk).toBeDefined()
}),
)
it.effect("ignores packages that are not the bundled Perplexity package", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/perplexity-compatible",
options: { name: "perplexity" },
})
expect(result.sdk).toBeUndefined()
}),
)
it.effect("uses the Perplexity provider ID as the SDK name for the bundled provider", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/perplexity",
options: { name: "perplexity" },
})
expect(result.sdk.languageModel("sonar").provider).toBe("perplexity")
}),
)
it.effect("creates bundled Perplexity SDKs for custom provider IDs", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")),
modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/perplexity",
options: { name: "custom-perplexity" },
})
expect(result.sdk.languageModel("sonar").provider).toBe("perplexity")
}),
)
it.effect("leaves Perplexity language selection to the default languageModel fallback", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = []
yield* addPlugin()
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
})
expect(calls).toEqual([])
expect(result.language).toBeUndefined()
}),
)
})
@@ -1,132 +0,0 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* TogetherAIPlugin.effect(host)
})
function fakeSelectorSdk(calls: string[]) {
const make = (method: string) => (id: string) => {
calls.push(`${method}:${id}`)
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
}
return {
responses: make("responses"),
messages: make("messages"),
chat: make("chat"),
languageModel: make("languageModel"),
}
}
describe("TogetherAIPlugin", () => {
it.effect("creates a TogetherAI SDK for @ai-sdk/togetherai", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/togetherai",
options: { name: "togetherai" },
})
expect(result.sdk).toBeDefined()
}),
)
it.effect("matches the old bundled provider package exactly", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}),
package: "file:///tmp/@ai-sdk/togetherai-provider.js",
options: { name: "togetherai" },
})
expect(ignored.sdk).toBeUndefined()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/togetherai",
options: { name: "togetherai" },
})
expect(result.sdk).toBeDefined()
}),
)
it.effect("creates bundled TogetherAI SDKs for custom provider IDs", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/togetherai",
options: { name: "custom-togetherai" },
})
expect(result.sdk.languageModel("model").provider).toBe("togetherai.chat")
}),
)
it.effect("defaults language selection to sdk.languageModel with the model API ID", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = []
yield* addPlugin()
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(
ProviderV2.ID.make("togetherai"),
ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
),
modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
package: "aisdk:test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
})
expect(result.language).toBeUndefined()
expect(calls).toEqual([])
expect(
result.language ?? fakeSelectorSdk(calls).languageModel(result.model.modelID ?? result.model.id),
).toBeDefined()
expect(calls).toEqual(["languageModel:meta-llama/Llama-3.3-70B-Instruct-Turbo"])
}),
)
})
@@ -1,120 +0,0 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const host = yield* PluginHost.make(plugin)
yield* VenicePlugin.effect(host)
})
function fakeSelectorSdk(calls: string[]) {
const make = (method: string) => (id: string) => {
calls.push(`${method}:${id}`)
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
}
return {
responses: make("responses"),
messages: make("messages"),
chat: make("chat"),
languageModel: make("languageModel"),
}
}
describe("VenicePlugin", () => {
it.effect("creates a Venice SDK for venice-ai-sdk-provider", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}),
package: "venice-ai-sdk-provider",
options: { name: "venice" },
})
expect(result.sdk).toBeDefined()
}),
)
it.effect("uses the model provider ID as the bundled Venice SDK name", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}),
package: "venice-ai-sdk-provider",
options: { name: "custom-venice", apiKey: "test" },
})
expect(result.sdk).toBeDefined()
expect(result.sdk.languageModel("model").provider).toBe("custom-venice.chat")
}),
)
it.effect("only handles the bundled venice-ai-sdk-provider package", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const similar = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}),
package: "file:///tmp/venice-ai-sdk-provider.js",
options: { name: "venice" },
})
const other = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "venice" },
})
expect(similar.sdk).toBeUndefined()
expect(other.sdk).toBeUndefined()
}),
)
it.effect("leaves Venice language selection to the default languageModel fallback", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = []
yield* addPlugin()
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("alias"),
package: "aisdk:test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
})
expect(calls).toEqual([])
expect(result.language).toBeUndefined()
}),
)
})
+25
View File
@@ -66,6 +66,31 @@ describe("ProjectV2.list", () => {
)
})
describe("ProjectV2.update", () => {
it.effect("updates project metadata", () =>
Effect.gen(function* () {
const db = (yield* Database.Service).db
const project = yield* ProjectV2.Service
const id = ProjectV2.ID.make("updated")
yield* db
.insert(ProjectTable)
.values({ id, worktree: abs("/updated"), sandboxes: [], time_created: 1, time_updated: 1 })
.run()
const result = yield* project.update(id, {
name: "Updated",
icon: { color: "#ffffff" },
commands: { start: "bun dev" },
})
expect(result.name).toBe("Updated")
expect(result.icon).toEqual({ color: "#ffffff" })
expect(result.commands).toEqual({ start: "bun dev" })
expect(result.time.updated).toBeGreaterThan(1)
}),
)
})
function remoteID(remote: string) {
return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`))
}
@@ -90,6 +90,18 @@ const waitForOutput = (output: Queue.Queue<string>, text: string) =>
}),
)
describe("Pty.shells", () => {
it.live("lists available shells", () =>
Effect.gen(function* () {
const pty = yield* Pty.Service
const shells = yield* pty.shells()
expect(shells.length).toBeGreaterThan(0)
expect(shells.every((shell) => shell.path && shell.name && typeof shell.acceptable === "boolean")).toBe(true)
}),
)
})
describe("pty", () => {
it.live("returns typed not found errors for missing sessions", () =>
Effect.gen(function* () {
+10 -8
View File
@@ -36,6 +36,7 @@ const projects = Layer.succeed(
list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
directories: () => Effect.succeed([]),
update: () => Effect.die("unused"),
commit: () => Effect.void,
}),
)
@@ -49,14 +50,15 @@ const client = Layer.mock(LLMClient.Service)({
generate: () => Effect.die("unused"),
})
const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
const models = SessionRunnerModel.layerWith(() =>
Effect.succeed(
SessionRunnerModel.resolved(model, {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [],
}),
),
)
const models = Layer.mock(SessionRunnerModel.Service)({
resolve: () =>
Effect.succeed(
SessionRunnerModel.resolved(model, {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [],
}),
),
})
const locations = Layer.effect(
LocationServiceMap.Service,
LayerMap.make(
+29
View File
@@ -34,6 +34,7 @@ const projects = Layer.succeed(
list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
directories: () => Effect.succeed([]),
update: () => Effect.die("unused"),
commit: () => Effect.void,
}),
)
@@ -660,4 +661,32 @@ describe("SessionV2.create", () => {
).toBe("Session.NotFoundError")
}),
)
it.effect("archives a Session through one durable event", () =>
Effect.gen(function* () {
const session = yield* SessionV2.Service
const created = yield* session.create({ location })
yield* session.archive(created.id)
yield* session.archive(created.id)
expect((yield* session.get(created.id)).time.archived).toBeDefined()
const events = Array.from(yield* logEvents(session, created.id).pipe(Stream.runCollect))
expect(events.map((event) => event.type)).toContain("session.archived")
expect(events.filter((event) => event.type === "session.archived")).toHaveLength(1)
}),
)
it.effect("rejects archiving a missing Session", () =>
Effect.gen(function* () {
const session = yield* SessionV2.Service
expect(
yield* session.archive(SessionV2.ID.make("ses_missing_archive")).pipe(
Effect.flip,
Effect.map((error) => error._tag),
),
).toBe("Session.NotFoundError")
}),
)
})
+9 -8
View File
@@ -66,14 +66,15 @@ const client = Layer.mock(LLMClient.Service)({
return response
}),
})
const models = SessionRunnerModel.layerWith(() =>
Effect.succeed(
SessionRunnerModel.resolved(model, {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [],
}),
),
)
const models = Layer.mock(SessionRunnerModel.Service)({
resolve: () =>
Effect.succeed(
SessionRunnerModel.resolved(model, {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [],
}),
),
})
const builtins = Layer.mock(InstructionBuiltIns.Service, {
load: () =>
Effect.succeed(
@@ -57,6 +57,7 @@ const projects = Layer.succeed(
list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
directories: () => Effect.succeed([]),
update: () => Effect.die("unused"),
commit: () => Effect.void,
}),
)
+1
View File
@@ -22,6 +22,7 @@ const projects = Layer.succeed(
list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
directories: () => Effect.succeed([]),
update: () => Effect.die("unused"),
commit: () => Effect.void,
}),
)
@@ -19,6 +19,7 @@ const projects = Layer.succeed(
list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
directories: () => Effect.succeed([]),
update: () => Effect.die("unused"),
commit: () => Effect.void,
}),
)
@@ -73,14 +73,15 @@ const model = OpenAIChat.route
generation: { maxTokens: 20, temperature: 0 },
})
.model({ id: "gpt-4o-mini" })
const models = SessionRunnerModel.layerWith(() =>
Effect.succeed(
SessionRunnerModel.resolved(model, {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [],
}),
),
)
const models = Layer.mock(SessionRunnerModel.Service)({
resolve: () =>
Effect.succeed(
SessionRunnerModel.resolved(model, {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [],
}),
),
})
const systemContext = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed(Instructions.empty) })
const instructionContext = Layer.mock(InstructionDiscovery.Service, { load: () => Effect.succeed(Instructions.empty) })
const skillInstructions = Layer.mock(SkillInstructions.Service, { load: () => Effect.succeed(Instructions.empty) })
+11 -10
View File
@@ -280,17 +280,18 @@ const echo = Layer.effectDiscard(
const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] })
let modelResolveHook = Effect.void
let currentModel = model
const models = SessionRunnerModel.layerWith((session) =>
modelResolveHook.pipe(
Effect.as(
SessionRunnerModel.resolved(session.model?.id === "replacement" ? replacementModel : currentModel, {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [],
variant: session.model?.variant,
}),
const models = Layer.mock(SessionRunnerModel.Service)({
resolve: (session) =>
modelResolveHook.pipe(
Effect.as(
SessionRunnerModel.resolved(session.model?.id === "replacement" ? replacementModel : currentModel, {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [],
variant: session.model?.variant,
}),
),
),
),
)
})
const systemContextKey = Instructions.Key.make("test/context")
let systemBaseline = "Initial context"
let systemRemoved = false
-163
View File
@@ -387,17 +387,6 @@ describe("PatchTool", () => {
),
)
it.live("updates an empty file", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "empty.txt")
yield* Effect.promise(() => fs.writeFile(target, ""))
yield* executeTool(registry, call("*** Begin Patch\n*** Update File: empty.txt\n@@\n+First line\n*** End Patch"))
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("First line\n")
}),
),
)
it.live("rejects deleting a directory", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
@@ -410,40 +399,6 @@ describe("PatchTool", () => {
),
)
it.live("supports an end-of-file anchor", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "tail.txt")
yield* Effect.promise(() => fs.writeFile(target, "first\nsecond"))
yield* executeTool(
registry,
call(
"*** Begin Patch\n*** Update File: tail.txt\n@@\n first\n-second\n+second updated\n*** End of File\n*** End Patch",
),
)
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("first\nsecond updated\n")
}),
),
)
it.live("applies an end-of-file chunk to the final duplicate", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "duplicates.txt")
yield* Effect.promise(() => fs.writeFile(target, "marker\nend\nmiddle\nmarker\nend\n"))
yield* executeTool(
registry,
call(
"*** Begin Patch\n*** Update File: duplicates.txt\n@@\n-marker\n-end\n+marker changed\n+end\n*** End of File\n*** End Patch",
),
)
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe(
"marker\nend\nmiddle\nmarker changed\nend\n",
)
}),
),
)
it.live("rejects a missing second chunk context", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
@@ -513,20 +468,6 @@ describe("PatchTool", () => {
),
)
it.live("applies multiple hunks to one file", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "multi.txt")
yield* Effect.promise(() => fs.writeFile(target, "a\nb\nc\nd\n"))
yield* executeTool(
registry,
call("*** Begin Patch\n*** Update File: multi.txt\n@@\n-b\n+B\n@@\n-d\n+D\n*** End Patch"),
)
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("a\nB\nc\nD\n")
}),
),
)
it.live("applies successive update operations to one file", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
@@ -566,110 +507,6 @@ describe("PatchTool", () => {
),
)
it.live("appends a trailing newline on update", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "no-newline.txt")
yield* Effect.promise(() => fs.writeFile(target, "no newline at end"))
yield* executeTool(
registry,
call(
"*** Begin Patch\n*** Update File: no-newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch",
),
)
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("first line\nsecond line\n")
}),
),
)
it.live("disambiguates change context with an @@ header", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "context.txt")
yield* Effect.promise(() => fs.writeFile(target, "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n"))
yield* executeTool(
registry,
call("*** Begin Patch\n*** Update File: context.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch"),
)
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe(
"fn a\nx=10\ny=2\nfn b\nx=11\ny=20\n",
)
}),
),
)
it.live("parses a heredoc-wrapped patch", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
yield* executeTool(
registry,
call("cat <<'EOF'\n*** Begin Patch\n*** Add File: heredoc.txt\n+with cat\n*** End Patch\nEOF"),
)
expect(yield* Effect.promise(() => fs.readFile(path.join(directory, "heredoc.txt"), "utf8"))).toBe(
"with cat\n",
)
}),
),
)
it.live("parses a heredoc-wrapped patch without cat", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
yield* executeTool(
registry,
call("<<EOF\n*** Begin Patch\n*** Add File: heredoc.txt\n+without cat\n*** End Patch\nEOF"),
)
expect(yield* Effect.promise(() => fs.readFile(path.join(directory, "heredoc.txt"), "utf8"))).toBe(
"without cat\n",
)
}),
),
)
it.live("matches with trailing whitespace differences", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "trailing.txt")
yield* Effect.promise(() => fs.writeFile(target, "line1 \nline2\nline3 \n"))
yield* executeTool(
registry,
call("*** Begin Patch\n*** Update File: trailing.txt\n@@\n-line2\n+changed\n*** End Patch"),
)
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("line1 \nchanged\nline3 \n")
}),
),
)
it.live("matches with leading whitespace differences", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "leading.txt")
yield* Effect.promise(() => fs.writeFile(target, " line1\nline2\n line3\n"))
yield* executeTool(
registry,
call("*** Begin Patch\n*** Update File: leading.txt\n@@\n-line2\n+changed\n*** End Patch"),
)
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe(" line1\nchanged\n line3\n")
}),
),
)
it.live("matches with Unicode punctuation differences", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
const target = path.join(directory, "unicode.txt")
yield* Effect.promise(() => fs.writeFile(target, "He said “hello”\nsome—dash\nend\n"))
yield* executeTool(
registry,
call(
'*** Begin Patch\n*** Update File: unicode.txt\n@@\n-He said "hello"\n+He said "hi"\n*** End Patch',
),
)
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe('He said "hi"\nsome—dash\nend\n')
}),
),
)
it.live("rejects an update with missing context", () =>
withTempTool((directory, registry) =>
Effect.gen(function* () {
+2 -2
View File
@@ -88,8 +88,8 @@ describe("search tools", () => {
expect(glob.output?.structured).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT })
expect(grep.output?.structured).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT })
expect(glob.output?.content).toEqual([{ type: "text", text: glob.result.value }])
expect(grep.output?.content).toEqual([{ type: "text", text: grep.result.value }])
expect(glob.output?.content).toEqual([{ type: "text", text: String(glob.result.value) }])
expect(grep.output?.content).toEqual([{ type: "text", text: String(grep.result.value) }])
expect(String(glob.result.value).split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT)
expect(grep.result.value).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`)
}),
+1
View File
@@ -152,6 +152,7 @@ describeHg("Vcs mercurial", () => {
await commitAll(directory, "initial")
})
const vcs = yield* Vcs.Service
expect(yield* vcs.info()).toEqual({ branch: "default", defaultBranch: "default" })
expect(yield* vcs.diff("branch")).toEqual([])
yield* Effect.promise(async () => {
+2
View File
@@ -53,6 +53,7 @@ describe("Vcs", () => {
withTmp((directory) =>
Effect.gen(function* () {
const vcs = yield* Vcs.Service
expect(yield* vcs.info()).toEqual({})
expect(yield* vcs.status()).toEqual([])
expect(yield* vcs.diff("working")).toEqual([])
expect(yield* vcs.diff("branch")).toEqual([])
@@ -155,6 +156,7 @@ describe("Vcs", () => {
await commitAll(directory, "initial")
})
const vcs = yield* Vcs.Service
expect(yield* vcs.info()).toEqual({ branch: "main", defaultBranch: "main" })
expect(yield* vcs.diff("branch")).toEqual([])
yield* Effect.promise(async () => {
+11
View File
@@ -19,4 +19,15 @@ bun validate
bun broken-links
```
The V2 theme token reference is generated from
`packages/tui/src/theme/v2/schema.ts`. Regenerate it after schema changes:
```bash
bun run generate
```
`bun validate` checks that the committed snippet is current. The repository's
generation workflow also refreshes it on pushes to `dev`, so Mintlify always
receives the generated MDX as part of the published docs tree.
The hosted preview is available at [opencode.mintlify.site](https://opencode.mintlify.site).
+3 -4
View File
@@ -89,7 +89,7 @@ becomes `system`:
description: Reviews changes without modifying files
mode: subagent
model: anthropic/claude-sonnet-4-5#high
color: warning
color: "#ff6b6b"
steps: 8
permissions:
- action: edit
@@ -118,7 +118,7 @@ Use the `agents` field in any [OpenCode configuration file](/config):
"mode": "all",
"model": "anthropic/claude-sonnet-4-5#high",
"system": "Review the current changes. Report findings before any summary.",
"color": "warning",
"color": "#ff6b6b",
"steps": 8,
"permissions": [
{ "action": "edit", "resource": "*", "effect": "deny" },
@@ -250,8 +250,7 @@ security boundary.
### `color`
Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`, or one
of `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info`.
Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`.
### `disabled`
+1
View File
@@ -38,6 +38,7 @@
"attachments",
"compaction",
"warming",
"themes",
"formatters",
"lsp",
"references"
+1 -1
View File
@@ -158,6 +158,6 @@ limitations and safety details.
## Customize
Make OpenCode your own by [picking a theme](https://opencode.ai/docs/themes), [customizing
Make OpenCode your own by [picking a theme](/themes), [customizing
keybinds](https://opencode.ai/docs/keybinds), [configuring formatters](/formatters), [creating commands](/commands), or
editing the [OpenCode config](/config).
+3 -19
View File
@@ -10561,26 +10561,10 @@
"additionalProperties": false
},
"Agent.Color": {
"anyOf": [
"type": "string",
"allOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^#[0-9a-fA-F]{6}$"
}
]
},
{
"type": "string",
"enum": [
"primary",
"secondary",
"accent",
"success",
"warning",
"error",
"info"
]
"pattern": "^#[0-9a-fA-F]{6}$"
}
]
},
+7 -3
View File
@@ -3,11 +3,15 @@
"name": "@opencode-ai/docs",
"private": true,
"scripts": {
"dev": "bun --bun mint dev --no-open --port 3333",
"validate": "bun --bun mint validate",
"dev": "bun run generate && bun --bun mint dev --no-open --port 3333",
"generate": "bun script/generate-theme-tokens.ts",
"check:generated": "bun script/generate-theme-tokens.ts --check",
"validate": "bun run check:generated && bun --bun mint validate",
"broken-links": "bun --bun mint broken-links"
},
"devDependencies": {
"mint": "4.2.666"
"effect": "catalog:",
"mint": "4.2.666",
"prettier": "3.6.2"
}
}
@@ -0,0 +1,136 @@
#!/usr/bin/env bun
import { Schema, SchemaAST } from "effect"
import { format } from "prettier"
import { ThemeDefinition, ThemeFile } from "../../tui/src/theme/v2/schema"
const target = import.meta.dir + "/../snippets/generated/theme-tokens.mdx"
const root = requireObject(ThemeDefinition.ast)
const hue = requireObject(requireField(root, "hue").type)
const hueNames = hue.propertySignatures.map((field) => String(field.name))
const hueSteps = requireObject(requireField(hue, hueNames[0]).type).propertySignatures.map((field) =>
String(field.name),
)
const contexts = root.propertySignatures
.map((field) => String(field.name))
.filter((name) => name.startsWith("@context:"))
const tokens = root.propertySignatures
.filter((field) => {
const name = String(field.name)
return name !== "hue" && name !== "categorical" && !name.startsWith("@context:")
})
.flatMap((field) => tokenPaths(field.type, String(field.name)))
const groups = Map.groupBy(tokens, (token) =>
token
.split(".")
.slice(0, token.split(".").length > 2 ? 2 : 1)
.join("."),
)
const table = [...groups]
.map(([group, values]) => `| \`${group}\` | ${values.map((value) => `\`${value}\``).join("<br />")} |`)
.join("\n")
const example = {
version: 2,
light: {
hue: {
accent: "$hue.purple",
interactive: "$hue.purple",
},
text: {
default: "$hue.neutral.900",
},
background: {
default: "#fafafa",
},
},
dark: {
mergeMode: true,
text: {
default: "$hue.neutral.100",
},
background: {
default: "#101014",
},
},
} satisfies ThemeFile
Schema.decodeUnknownSync(ThemeFile)(example)
const output = await format(
`{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */}
\`\`\`json title="my-theme.json"
${JSON.stringify(example, null, 2)}
\`\`\`
## Token reference
This reference is generated from the Effect schema in
\`packages/tui/src/theme/v2/schema.ts\`. Changes to the runtime schema update
this section through \`bun run generate\`.
### Hue tokens
Every hue is a ${hueSteps.length}-step scale. Define a scale with all of these
steps, or alias it to another hue with a value such as \`$hue.blue\`.
| | Values |
| --- | --- |
| Hues | ${hueNames.map((name) => `\`${name}\``).join(", ")} |
| Steps | ${hueSteps.map((step) => `\`${step}\``).join(", ")} |
Reference a hue color as \`$hue.<name>.<step>\`, for example
\`$hue.interactive.500\`.
### Semantic tokens
Semantic values can reference another token by prefixing its path with \`$\`,
for example \`$text.default\`. Stateful tokens inherit their \`default\`
value when a state is omitted.
| Group | Tokens |
| --- | --- |
${table}
### Contexts
${contexts.map((context) => `\`${context}\``).join(" and ")} accept partial
overrides of the semantic tokens above. Components apply these contexts to
surfaces that need different contrast without changing the base theme.
`,
{ parser: "mdx", printWidth: 120, semi: false },
)
if (process.argv.includes("--check")) {
const current = await Bun.file(target).text()
if (current === output) process.exit(0)
console.error("Generated theme token documentation is stale. Run `bun run generate` from packages/docs.")
process.exit(1)
}
await Bun.write(target, output)
function requireObject(ast: SchemaAST.AST): SchemaAST.Objects {
if (SchemaAST.isObjects(ast)) return ast
if (SchemaAST.isUnion(ast)) {
const object = ast.types.map(findObject).find((value) => value !== undefined)
if (object) return object
}
throw new Error(`Expected an object schema, received ${ast._tag}`)
}
function findObject(ast: SchemaAST.AST): SchemaAST.Objects | undefined {
if (SchemaAST.isObjects(ast)) return ast
if (SchemaAST.isUnion(ast)) return ast.types.map(findObject).find((value) => value !== undefined)
if (SchemaAST.isSuspend(ast)) return findObject(ast.thunk())
}
function requireField(ast: SchemaAST.Objects, name: string) {
const field = ast.propertySignatures.find((field) => String(field.name) === name)
if (field) return field
throw new Error(`Theme schema field not found: ${name}`)
}
function tokenPaths(ast: SchemaAST.AST, prefix: string): string[] {
const object = findObject(ast)
if (!object || object.propertySignatures.length === 0) return [prefix]
return object.propertySignatures.flatMap((field) => tokenPaths(field.type, `${prefix}.${String(field.name)}`))
}
@@ -0,0 +1,79 @@
{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */}
```json title="my-theme.json"
{
"version": 2,
"light": {
"hue": {
"accent": "$hue.purple",
"interactive": "$hue.purple"
},
"text": {
"default": "$hue.neutral.900"
},
"background": {
"default": "#fafafa"
}
},
"dark": {
"mergeMode": true,
"text": {
"default": "$hue.neutral.100"
},
"background": {
"default": "#101014"
}
}
}
```
## Token reference
This reference is generated from the Effect schema in
`packages/tui/src/theme/v2/schema.ts`. Changes to the runtime schema update
this section through `bun run generate`.
### Hue tokens
Every hue is a 9-step scale. Define a scale with all of these
steps, or alias it to another hue with a value such as `$hue.blue`.
| | Values |
| ----- | -------------------------------------------------------------------------------------------------------- |
| Hues | `gray`, `red`, `orange`, `yellow`, `green`, `cyan`, `blue`, `purple`, `accent`, `interactive`, `neutral` |
| Steps | `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900` |
Reference a hue color as `$hue.<name>.<step>`, for example
`$hue.interactive.500`.
### Semantic tokens
Semantic values can reference another token by prefixing its path with `$`,
for example `$text.default`. Stateful tokens inherit their `default`
value when a state is omitted.
| Group | Tokens |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text` | `text.default`<br />`text.subdued` |
| `text.action` | `text.action.primary.default`<br />`text.action.primary.$hovered`<br />`text.action.primary.$focused`<br />`text.action.primary.$pressed`<br />`text.action.primary.$selected`<br />`text.action.primary.$disabled`<br />`text.action.destructive.default`<br />`text.action.destructive.$hovered`<br />`text.action.destructive.$focused`<br />`text.action.destructive.$pressed`<br />`text.action.destructive.$selected`<br />`text.action.destructive.$disabled` |
| `text.formfield` | `text.formfield.default`<br />`text.formfield.$hovered`<br />`text.formfield.$focused`<br />`text.formfield.$pressed`<br />`text.formfield.$selected`<br />`text.formfield.$disabled` |
| `text.feedback` | `text.feedback.error.default`<br />`text.feedback.error.subdued`<br />`text.feedback.warning.default`<br />`text.feedback.warning.subdued`<br />`text.feedback.success.default`<br />`text.feedback.success.subdued`<br />`text.feedback.info.default`<br />`text.feedback.info.subdued` |
| `background` | `background.default` |
| `background.surface` | `background.surface.offset`<br />`background.surface.overlay` |
| `background.action` | `background.action.primary.default`<br />`background.action.primary.$hovered`<br />`background.action.primary.$focused`<br />`background.action.primary.$pressed`<br />`background.action.primary.$selected`<br />`background.action.primary.$disabled`<br />`background.action.destructive.default`<br />`background.action.destructive.$hovered`<br />`background.action.destructive.$focused`<br />`background.action.destructive.$pressed`<br />`background.action.destructive.$selected`<br />`background.action.destructive.$disabled` |
| `background.formfield` | `background.formfield.default`<br />`background.formfield.$hovered`<br />`background.formfield.$focused`<br />`background.formfield.$pressed`<br />`background.formfield.$selected`<br />`background.formfield.$disabled` |
| `background.feedback` | `background.feedback.error.default`<br />`background.feedback.warning.default`<br />`background.feedback.success.default`<br />`background.feedback.info.default` |
| `border` | `border.default` |
| `scrollbar` | `scrollbar.default` |
| `diff.text` | `diff.text.added`<br />`diff.text.removed`<br />`diff.text.context`<br />`diff.text.hunkHeader` |
| `diff.background` | `diff.background.added`<br />`diff.background.removed`<br />`diff.background.context` |
| `diff.highlight` | `diff.highlight.added`<br />`diff.highlight.removed` |
| `diff.lineNumber` | `diff.lineNumber.text`<br />`diff.lineNumber.background.added`<br />`diff.lineNumber.background.removed` |
| `syntax` | `syntax.comment`<br />`syntax.keyword`<br />`syntax.function`<br />`syntax.variable`<br />`syntax.string`<br />`syntax.number`<br />`syntax.type`<br />`syntax.operator`<br />`syntax.punctuation` |
| `markdown` | `markdown.text`<br />`markdown.heading`<br />`markdown.link`<br />`markdown.linkText`<br />`markdown.code`<br />`markdown.blockQuote`<br />`markdown.emphasis`<br />`markdown.strong`<br />`markdown.horizontalRule`<br />`markdown.listItem`<br />`markdown.listEnumeration`<br />`markdown.image`<br />`markdown.imageText`<br />`markdown.codeBlock` |
### Contexts
`@context:elevated` and `@context:overlay` accept partial
overrides of the semantic tokens above. Components apply these contexts to
surfaces that need different contrast without changing the base theme.
+129
View File
@@ -0,0 +1,129 @@
---
title: "Themes"
description: "Choose a built-in TUI theme or create a custom color scheme."
---
import ThemeTokens from "/snippets/generated/theme-tokens.mdx"
OpenCode includes built-in light and dark themes and can load custom themes
from your global configuration or a project directory. The default theme is
`opencode`.
## Choose a theme
In the full-screen TUI, run:
```text
/themes
```
You can also open the picker with `ctrl+x`, then `t`, using the
default keybindings.
Use `/settings` to change both the theme and its color mode. OpenCode supports
three modes:
| Mode | Behavior |
| -------- | -------------------------------------------------------- |
| `system` | Follow the terminal's detected light or dark appearance. |
| `dark` | Always use the theme's dark colors. |
| `light` | Always use the theme's light colors. |
Your selection is stored in `~/.config/opencode/cli.json`, or the equivalent
path under `$XDG_CONFIG_HOME`:
```json title="cli.json"
{
"theme": {
"name": "tokyonight",
"mode": "system"
}
}
```
<Note>
Theme selection applies to the full-screen TUI. Direct interactive runs use colors derived from the terminal palette
and honor only the color mode.
</Note>
## Built-in themes
OpenCode currently includes:
| | | |
| ------------ | ------------------- | ---------------------- |
| `aura` | `ayu` | `carbonfox` |
| `catppuccin` | `catppuccin-frappe` | `catppuccin-macchiato` |
| `cobalt2` | `cursor` | `dracula` |
| `everforest` | `flexoki` | `github` |
| `gruvbox` | `kanagawa` | `lucent-orng` |
| `material` | `matrix` | `mercury` |
| `monokai` | `nightowl` | `nord` |
| `one-dark` | `opencode` | `orng` |
| `osaka-jade` | `palenight` | `rosepine` |
| `solarized` | `synthwave84` | `tokyonight` |
| `vercel` | `vesper` | `zenburn` |
When OpenCode can read your terminal palette, the picker also includes
`system`. The `system` theme generates its colors from your terminal's
foreground, background, and ANSI palette.
## Custom themes
Create a JSON file in either of these locations:
```text
~/.config/opencode/themes/my-theme.json
.opencode/themes/my-theme.json
```
OpenCode checks the global theme directory first, followed by every
`.opencode/themes` directory from the filesystem root down to the current
directory. A more local file with the same filename overrides an earlier one.
The filename becomes the theme name, so `my-theme.json` appears as `my-theme`.
Custom theme files must be strict JSON. Comments and trailing commas are not
supported.
### Format
V2 themes organize colors into hue scales and semantic tokens. Set `version`
to `2` and define at least one of `light` or `dark`:
<Warning>
Native V2 custom theme files are not loaded directly by the current beta. Existing custom files use the V1 format and
are migrated to these tokens at runtime. This reference tracks the native V2 schema while direct file loading is
completed.
</Warning>
By default, a theme inherits OpenCode's complete theme, so you only need to
define overrides. Set `mergeMode` to `true` to inherit one mode from the other
before applying that mode's overrides. Set `standalone` to `true` only when you
intend to supply a complete independent theme.
Each token accepts:
- A hex color such as `"#5c9cf5"`
- `"transparent"` to use the terminal default
- A hue reference such as `"$hue.blue.500"`
- Another semantic token reference such as `"$text.default"`
Syntax and markdown tokens accept hex colors and hue references. Other
semantic tokens can reference any semantic token.
<ThemeTokens />
If you add or edit a custom theme while OpenCode is running, restart the TUI to
reload it.
## Terminal colors
Themes display most accurately in a terminal with truecolor support. Check
your terminal with:
```bash
echo $COLORTERM
```
Most modern terminals report `truecolor` or `24bit`. Without truecolor,
OpenCode approximates theme colors using the available terminal palette.
+3
View File
@@ -30,6 +30,7 @@ import { CredentialGroup } from "./groups/credential.js"
import { ProjectGroup } from "./groups/project.js"
import { ProjectCopyGroup } from "./groups/project-copy.js"
import { VcsGroup } from "./groups/vcs.js"
import { PathGroup } from "./groups/path.js"
type LocationGroups<LocationId extends HttpApiMiddleware.AnyId> =
| HttpApiGroup.AddMiddleware<typeof LocationGroup, LocationId>
@@ -50,6 +51,7 @@ type LocationGroups<LocationId extends HttpApiMiddleware.AnyId> =
| HttpApiGroup.AddMiddleware<typeof ReferenceGroup, LocationId>
| HttpApiGroup.AddMiddleware<typeof ProjectCopyGroup, LocationId>
| HttpApiGroup.AddMiddleware<typeof VcsGroup, LocationId>
| HttpApiGroup.AddMiddleware<typeof PathGroup, LocationId>
type SessionGroups<SessionLocationId extends HttpApiMiddleware.AnyId, SessionLocationService> =
| ReturnType<typeof makeSessionGroup<SessionLocationId, SessionLocationService>>
@@ -168,6 +170,7 @@ const makeApiFromGroup = <
.add(ReferenceGroup.middleware(locationMiddleware))
.add(ProjectCopyGroup.middleware(locationMiddleware))
.add(VcsGroup.middleware(locationMiddleware))
.add(PathGroup.middleware(locationMiddleware))
.add(DebugGroup)
.annotateMerge(
OpenApi.annotations({
+3 -2
View File
@@ -60,7 +60,8 @@ export const groupNames = {
"server.project": "project",
"server.projectCopy": "projectCopy",
"server.vcs": "vcs",
"server.path": "path",
} as const
export const promiseOmitEndpoints = new Set(["pty.connect", "pty.connectToken"])
export const effectOmitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"])
export const promiseOmitEndpoints = new Set(["pty.connect"])
export const effectOmitEndpoints = new Set(["fs.read", "pty.connect"])
+10
View File
@@ -1,5 +1,6 @@
import { Schema } from "effect"
import { Skill } from "@opencode-ai/schema/skill"
import { Project } from "@opencode-ai/schema/project"
export class InvalidRequestError extends Schema.TaggedErrorClass<InvalidRequestError>()(
"InvalidRequestError",
@@ -62,6 +63,15 @@ export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFo
{ httpApiStatus: 404 },
) {}
export class ProjectNotFoundError extends Schema.TaggedErrorClass<ProjectNotFoundError>()(
"ProjectNotFoundError",
{
projectID: Project.ID,
message: Schema.String,
},
{ httpApiStatus: 404 },
) {}
export class SessionNotFoundError extends Schema.TaggedErrorClass<SessionNotFoundError>()(
"SessionNotFoundError",
{
+20
View File
@@ -0,0 +1,20 @@
import { Path } from "@opencode-ai/schema/path"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
export const PathGroup = HttpApiGroup.make("server.path")
.add(
HttpApiEndpoint.get("path.get", "/api/path", {
query: LocationQuery,
success: Path.Info,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.path.get",
summary: "Get paths",
description: "Get process and location paths.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "path", description: "Location and process paths." }))
+15
View File
@@ -2,6 +2,7 @@ import { Project } from "@opencode-ai/schema/project"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
import { ProjectNotFoundError } from "../errors.js"
const root = "/api/project"
@@ -17,6 +18,20 @@ export const ProjectGroup = HttpApiGroup.make("server.project")
}),
),
)
.add(
HttpApiEndpoint.patch("project.update", `${root}/:projectID`, {
params: { projectID: Project.ID },
payload: Project.UpdateInput,
success: Project.Info,
error: ProjectNotFoundError,
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.project.update",
summary: "Update project",
description: "Update project metadata.",
}),
),
)
.add(
HttpApiEndpoint.get("project.current", `${root}/current`, {
query: LocationQuery,
+16 -1
View File
@@ -19,6 +19,20 @@ export function hasPtyConnectTicketURL(url: URL) {
}
export const PtyGroup = HttpApiGroup.make("server.pty")
.add(
HttpApiEndpoint.get("pty.shells", "/api/pty/shells", {
query: LocationQuery,
success: Location.response(Schema.Array(Pty.Shell)),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.pty.shells",
summary: "List available shells",
description: "List shells available for interactive terminal sessions.",
}),
),
)
.add(
HttpApiEndpoint.get("pty.list", "/api/pty", {
query: LocationQuery,
@@ -101,13 +115,14 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
HttpApiEndpoint.post("pty.connectToken", "/api/pty/:ptyID/connect-token", {
params: { ptyID: Pty.ID },
query: LocationQuery,
headers: Schema.Struct({ [PTY_CONNECT_TOKEN_HEADER]: Schema.optional(Schema.String) }),
success: Location.response(PtyTicket.ConnectToken),
error: [ForbiddenError, PtyNotFoundError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.pty.connect.token",
identifier: "v2.pty.connectToken",
summary: "Create PTY WebSocket token",
description: "Create a short-lived single-use ticket for opening a PTY WebSocket connection.",
}),
+15
View File
@@ -269,6 +269,21 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
}),
),
)
.add(
HttpApiEndpoint.post("session.archive", "/api/session/:sessionID/archive", {
params: { sessionID: Session.ID },
success: HttpApiSchema.NoContent,
error: SessionNotFoundError,
})
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.archive",
summary: "Archive session",
description: "Archive a session.",
}),
),
)
.add(
HttpApiEndpoint.post("session.move", "/api/session/:sessionID/move", {
params: { sessionID: Session.ID },
+14
View File
@@ -13,6 +13,20 @@ const DiffQuery = Schema.Struct({
})
export const VcsGroup = HttpApiGroup.make("server.vcs")
.add(
HttpApiEndpoint.get("vcs.get", "/api/vcs", {
query: LocationQuery,
success: Location.response(Vcs.Info),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.vcs.get",
summary: "VCS information",
description: "Get the current and default branches for the requested location.",
}),
),
)
.add(
HttpApiEndpoint.get("vcs.status", "/api/vcs/status", {
query: LocationQuery,
+1 -4
View File
@@ -16,10 +16,7 @@ export type ID = typeof ID.Type
export const Name = Schema.String.pipe(Schema.brand("Agent.Name"))
export type Name = typeof Name.Type
export const Color = Schema.Union([
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
]).annotate({ identifier: "Agent.Color" })
export const Color = Schema.String.annotate({ identifier: "Agent.Color" })
export type Color = typeof Color.Type
export interface Info extends Schema.Schema.Type<typeof Info> {}
+1
View File
@@ -16,6 +16,7 @@ export const Info = Schema.Struct({
agent: Agent.ID.pipe(optional),
model: Model.Ref.pipe(optional),
subtask: Schema.Boolean.pipe(optional),
source: Schema.Literals(["command", "mcp"]).pipe(optional),
}).annotate({ identifier: "Command.Info" })
export const Event = {
+1
View File
@@ -14,6 +14,7 @@ export { Model } from "./model.js"
export { Money } from "./money.js"
export { Permission } from "./permission.js"
export { PermissionSaved } from "./permission-saved.js"
export { Path } from "./path.js"
export { Project } from "./project.js"
export { ProjectCopy } from "./project-copy.js"
export { Provider } from "./provider.js"
+13
View File
@@ -0,0 +1,13 @@
export * as Path from "./path.js"
import { Schema } from "effect"
import { AbsolutePath } from "./schema.js"
export const Info = Schema.Struct({
home: AbsolutePath,
state: AbsolutePath,
config: AbsolutePath,
worktree: AbsolutePath,
directory: AbsolutePath,
}).annotate({ identifier: "Path.Info" })
export interface Info extends Schema.Schema.Type<typeof Info> {}
+7
View File
@@ -56,5 +56,12 @@ export const Info = Schema.Struct({
}).annotate({ identifier: "Project" })
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const UpdateInput = Schema.Struct({
name: optional(Schema.String),
icon: optional(Icon),
commands: optional(Commands),
}).annotate({ identifier: "Project.UpdateInput" })
export interface UpdateInput extends Schema.Schema.Type<typeof UpdateInput> {}
const Updated = ephemeral({ type: "project.updated", schema: Info.fields })
export const Event = { Updated, Definitions: inventory(Updated) }
+7
View File
@@ -56,3 +56,10 @@ export const UpdateInput = Schema.Struct({
),
})
export interface UpdateInput extends Schema.Schema.Type<typeof UpdateInput> {}
export const Shell = Schema.Struct({
path: Schema.String,
name: Schema.String,
acceptable: Schema.Boolean,
}).annotate({ identifier: "Pty.Shell" })
export interface Shell extends Schema.Schema.Type<typeof Shell> {}
+8
View File
@@ -85,6 +85,13 @@ export const Renamed = Event.durable({
})
export type Renamed = typeof Renamed.Type
export const Archived = Event.durable({
type: "session.archived",
...options,
schema: Base,
})
export type Archived = typeof Archived.Type
export const UsageRecorded = Event.durable({
type: "session.usage.recorded",
...options,
@@ -539,6 +546,7 @@ export const Definitions = Event.inventory(
ModelSelected,
Moved,
Renamed,
Archived,
UsageUpdated,
Deleted,
Forked,
+7 -1
View File
@@ -1,7 +1,7 @@
export * as Vcs from "./vcs.js"
import { Schema } from "effect"
import { NonNegativeInt } from "./schema.js"
import { NonNegativeInt, optional } from "./schema.js"
export const Mode = Schema.Literals(["working", "branch"]).annotate({ identifier: "Vcs.Mode" })
export type Mode = typeof Mode.Type
@@ -13,3 +13,9 @@ export const FileStatus = Schema.Struct({
status: Schema.Literals(["added", "deleted", "modified"]),
}).annotate({ identifier: "Vcs.FileStatus" })
export interface FileStatus extends Schema.Schema.Type<typeof FileStatus> {}
export const Info = Schema.Struct({
branch: optional(Schema.String),
defaultBranch: optional(Schema.String),
}).annotate({ identifier: "Vcs.Info" })
export interface Info extends Schema.Schema.Type<typeof Info> {}
+10
View File
@@ -0,0 +1,10 @@
import { expect, test } from "bun:test"
import { Schema } from "effect"
import { Agent } from "../src/agent.js"
test("Agent.Color preserves configured colors at the public boundary", () => {
const encode = Schema.encodeSync(Agent.Color)
expect(encode("info")).toBe("info")
expect(encode("custom-color")).toBe("custom-color")
})
@@ -20,6 +20,12 @@ import { PersistedRevert } from "../src/session-revert.js"
import { optional } from "../src/schema.js"
describe("contract hygiene", () => {
test("restricts agent colors to six-digit hex values", () => {
const decode = Schema.decodeUnknownSync(Agent.Color)
expect(decode("#ff6b6b")).toBe("#ff6b6b")
expect(() => decode("warning")).toThrow()
})
test("keeps absolute costs distinct from model rates", () => {
const usd = Money.USD.make(1)
const rate = Money.USDPerMillionTokens.make(1)
@@ -104,6 +104,7 @@ describe("public event manifest", () => {
"session.model.selected.1",
"session.moved.1",
"session.renamed.1",
"session.archived.1",
"session.usage.recorded.1",
"session.forked.2",
"session.input.promoted.1",
+2
View File
@@ -26,6 +26,7 @@ import { CredentialHandler } from "./handlers/credential"
import { ProjectHandler } from "./handlers/project"
import { ProjectCopyHandler } from "./handlers/project-copy"
import { VcsHandler } from "./handlers/vcs"
import { PathHandler } from "./handlers/path"
import { EventFeed } from "./event-feed"
export const handlers = Layer.mergeAll(
@@ -56,4 +57,5 @@ export const handlers = Layer.mergeAll(
ReferenceHandler,
ProjectCopyHandler,
VcsHandler,
PathHandler,
)
+22
View File
@@ -0,0 +1,22 @@
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Global } from "@opencode-ai/util/global"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api"
export const PathHandler = HttpApiBuilder.group(Api, "server.path", (handlers) =>
handlers.handle("path.get", () =>
Effect.gen(function* () {
const global = yield* Global.Service
const location = yield* Location.Service
return {
home: AbsolutePath.make(global.home),
state: AbsolutePath.make(global.state),
config: AbsolutePath.make(global.config),
worktree: location.project.directory,
directory: location.directory,
}
}),
),
)
+16
View File
@@ -3,10 +3,26 @@ import { Project } from "@opencode-ai/core/project"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api"
import { ProjectNotFoundError } from "@opencode-ai/protocol/errors"
export const ProjectHandler = HttpApiBuilder.group(Api, "server.project", (handlers) =>
handlers
.handle("project.list", () => Project.Service.use((project) => project.list()))
.handle("project.update", (ctx) =>
Effect.gen(function* () {
const project = yield* Project.Service
return yield* project.update(ctx.params.projectID, ctx.payload).pipe(
Effect.catchTag(
"Project.NotFoundError",
() =>
new ProjectNotFoundError({
projectID: ctx.params.projectID,
message: `Project not found: ${ctx.params.projectID}`,
}),
),
)
}),
)
.handle("project.current", () =>
Location.Service.use((location) =>
Effect.succeed({ id: location.project.id, directory: location.project.directory }),
+7
View File
@@ -29,6 +29,13 @@ export const PtyHandler = HttpApiBuilder.group(Api, "server.pty", (handlers) =>
const environment = yield* PtyEnvironment.Service
return handlers
.handle(
"pty.shells",
Effect.fn(function* () {
const pty = yield* Pty.Service
return yield* response(pty.shells())
}),
)
.handle(
"pty.list",
Effect.fn(function* () {

Some files were not shown because too many files have changed in this diff Show More