mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87786c5e88 | |||
| 62c47f467f | |||
| 639a5fe5b1 | |||
| 9afd059fa1 | |||
| 6bbf50b940 | |||
| ba2dc65d6b | |||
| 08e616bcad | |||
| a6761d4fe0 | |||
| 305466e5ef | |||
| 704e9ff3fb | |||
| dbe7b937ef | |||
| 9de001d92f | |||
| 3cd894d40a | |||
| 0e705e2a05 | |||
| 421d649019 | |||
| a7855aa169 | |||
| 6e8aefcfa0 | |||
| f1f0f47ee2 | |||
| 203b9f59b7 | |||
| 48bcbd09ef | |||
| d86f732df3 | |||
| a817fe5e6c | |||
| 381f6c47b4 | |||
| b6e14b5a74 | |||
| 36979c9641 | |||
| 4e067a2014 |
@@ -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'
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
+53
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", () =>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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}$"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 })
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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] })
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -59,6 +59,7 @@ describe("CommandV2", () => {
|
||||
providerID: ProviderV2.ID.make("anthropic"),
|
||||
variant: ModelV2.VariantID.make("high"),
|
||||
},
|
||||
source: "command",
|
||||
}),
|
||||
])
|
||||
}),
|
||||
|
||||
@@ -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" },
|
||||
})
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -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"),
|
||||
|
||||
+37
-93
@@ -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()
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -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* () {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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) })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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* () {
|
||||
|
||||
@@ -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`)
|
||||
}),
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"attachments",
|
||||
"compaction",
|
||||
"warming",
|
||||
"themes",
|
||||
"formatters",
|
||||
"lsp",
|
||||
"references"
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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}$"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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({
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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." }))
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
}),
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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> {}
|
||||
@@ -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) }
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user