chore: update merge branch with latest v2
This commit is contained in:
commit
1f3867cf44
143 changed files with 2429 additions and 2428 deletions
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
|
|
@ -97,6 +97,11 @@ jobs:
|
||||||
working-directory: packages/client
|
working-directory: packages/client
|
||||||
run: bun run check:generated
|
run: bun run check:generated
|
||||||
|
|
||||||
|
- name: Check generated documentation
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
working-directory: packages/docs
|
||||||
|
run: bun run check:generated
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
name: e2e (${{ matrix.settings.name }})
|
name: e2e (${{ matrix.settings.name }})
|
||||||
if: github.ref_name != 'v2' && github.head_ref != 'v2'
|
if: github.ref_name != 'v2' && github.head_ref != 'v2'
|
||||||
|
|
|
||||||
26
bun.lock
26
bun.lock
|
|
@ -124,7 +124,7 @@
|
||||||
"opencode2": "./bin/opencode2.cjs",
|
"opencode2": "./bin/opencode2.cjs",
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "0.21.0",
|
"@agentclientprotocol/sdk": "1.2.1",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
|
|
@ -362,7 +362,7 @@
|
||||||
"@aws-sdk/credential-providers": "3.1057.0",
|
"@aws-sdk/credential-providers": "3.1057.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@effect/sql-sqlite-bun": "catalog:",
|
"@effect/sql-sqlite-bun": "catalog:",
|
||||||
"@ff-labs/fff-bun": "0.9.4",
|
"@ff-labs/fff-bun": "0.10.1",
|
||||||
"@lydell/node-pty": "catalog:",
|
"@lydell/node-pty": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.29.0",
|
"@modelcontextprotocol/sdk": "1.29.0",
|
||||||
"@opencode-ai/ai": "workspace:*",
|
"@opencode-ai/ai": "workspace:*",
|
||||||
|
|
@ -1173,7 +1173,7 @@
|
||||||
|
|
||||||
"@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="],
|
"@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="],
|
||||||
|
|
||||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.21.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw=="],
|
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.2.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-jwYUdOQR7tc+Zfch53VL4JJyUNK/46q03uUTYb+PjECsmnNl94XFXOfYLJ8RBpMNidXd1rpOAVgb0vqD98xImA=="],
|
||||||
|
|
||||||
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
||||||
|
|
||||||
|
|
@ -1641,23 +1641,25 @@
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||||
|
|
||||||
"@ff-labs/fff-bin-darwin-arm64": ["@ff-labs/fff-bin-darwin-arm64@0.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xyivu2xB++O5xXDx5Qm50JsU2aXt8YgXlGVhH/HE7UMYDrE6L6f1RYdYs8Y0bn0D3D0+bFBrN5ELPszK9E4Wbw=="],
|
"@ff-labs/fff-bin-android-arm64": ["@ff-labs/fff-bin-android-arm64@0.10.1", "", { "os": "android", "cpu": "arm64" }, "sha512-6Bsaa6yKEd2HV1M2WtqSYhoZucKYffIUms6GYoPN48QHP8hZgO8GXJ85/JDxKlkCJsN2hw1ROiwQK0+xUHYhFQ=="],
|
||||||
|
|
||||||
"@ff-labs/fff-bin-darwin-x64": ["@ff-labs/fff-bin-darwin-x64@0.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xLooAhCnTDCipPSMMZz7kGF3lhRHx6aP5fb6DJ0Ipyw/w/UWJb+xITJFszUl/QnIBoJ/qjDc93/FZMo1dk6gVA=="],
|
"@ff-labs/fff-bin-darwin-arm64": ["@ff-labs/fff-bin-darwin-arm64@0.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7yUP+56sG3UTrLg7eepOD16yM2dgiD7g6Ase2XWQB7oXwLV7mBMylPKIli2pP3kHzfw9K+BS3WAwHKHJ2QhxYw=="],
|
||||||
|
|
||||||
"@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-m5+8vA+1veaUUWonwva1WsU6m1HRm8CpYUzr06KDB65mewlmPbqz7+Fh7hjEfiD8C4mHVHe6RysULvAH1yhsdw=="],
|
"@ff-labs/fff-bin-darwin-x64": ["@ff-labs/fff-bin-darwin-x64@0.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-9zb+P1xtyqu/jVklm5RKF2zm9doRRsBNbkF/a8S4aSqSJoNlQR8ZF7C129fzOLfffVAjjgcO2l8oJgxOzHYiwQ=="],
|
||||||
|
|
||||||
"@ff-labs/fff-bin-linux-arm64-musl": ["@ff-labs/fff-bin-linux-arm64-musl@0.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-EMeWm7CSTVkizy4ZEzUkLDP024tVcbCUthduuIhekFQRDsiaAze0YboIylWb9HBHJCZlCCoZrWAl4nnJbsX7AA=="],
|
"@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-KTwr9CUfCJv0vtG2xG+nMxDae/2NJY2/oVmtgSvTnH9baI6JprqsFGphbukx7mdW/QMPwBiYtoO8ZGoC7i92jA=="],
|
||||||
|
|
||||||
"@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-pglE0uLkhnlE6bStXqfgUjYTSj+2sVwXaPfoA0QksidAsQor6NRt8004mygzC9DPubgHq5B9QezPfEwigKaP9Q=="],
|
"@ff-labs/fff-bin-linux-arm64-musl": ["@ff-labs/fff-bin-linux-arm64-musl@0.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-oznmSpV+zjiAPiwqbA4y+SAsMaYvDhGllHiT9L4ebdQnSOfcQzlUwvo45OhBPwHBt47tIois4bkF/MITlDk54A=="],
|
||||||
|
|
||||||
"@ff-labs/fff-bin-linux-x64-musl": ["@ff-labs/fff-bin-linux-x64-musl@0.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-VNKxgl8qs3aTfXViX7lqRK1aLu311h8dtBFqG4Scv+9Oi7WprybUp5L7IZ8sxKERaDAaiJMXHodXa1c90QdK8w=="],
|
"@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-KDpl8lwSEOauP/6FSJIvnARzE+ILm2rVIRQAio9dc5nn56EvlsryBWry0c5V0Tbw1PV0lNrZ+bzcIiPuTacvzw=="],
|
||||||
|
|
||||||
"@ff-labs/fff-bin-win32-arm64": ["@ff-labs/fff-bin-win32-arm64@0.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-uFEt0aNL54vQxq1ivjxRuo+thnhS4wLqa4INl4VXnXJUmwB42XXxD+gsj7vzhBLLx4cFf0aWgy/+TVDR8yjZtQ=="],
|
"@ff-labs/fff-bin-linux-x64-musl": ["@ff-labs/fff-bin-linux-x64-musl@0.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-mZCpojVtGNDr/wdCUEAHdfrl6qORvKHv3Pw35TaOdshnG4wocoA5bBKTGj/zmAU11o0GMK4V+wUhdNrgqoE10w=="],
|
||||||
|
|
||||||
"@ff-labs/fff-bin-win32-x64": ["@ff-labs/fff-bin-win32-x64@0.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Yd2Eyxj+slWv+0QDW9/xBpu9FXq+hwD0rXQD5184/88d+xwWCLKhEP2w8I6OO9XCg+kLT79UJb+k0WwXUtBtMw=="],
|
"@ff-labs/fff-bin-win32-arm64": ["@ff-labs/fff-bin-win32-arm64@0.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-SbT76ETXC5AgV9J8sVDozKG8wzrrxsOn8lbBprlOtx+O5V5EYmS1W7BlsatTHy6ddQ3uU5oOYO04PWZBuP6wXg=="],
|
||||||
|
|
||||||
"@ff-labs/fff-bun": ["@ff-labs/fff-bun@0.9.4", "", { "optionalDependencies": { "@ff-labs/fff-bin-darwin-arm64": "0.9.4", "@ff-labs/fff-bin-darwin-x64": "0.9.4", "@ff-labs/fff-bin-linux-arm64-gnu": "0.9.4", "@ff-labs/fff-bin-linux-arm64-musl": "0.9.4", "@ff-labs/fff-bin-linux-x64-gnu": "0.9.4", "@ff-labs/fff-bin-linux-x64-musl": "0.9.4", "@ff-labs/fff-bin-win32-arm64": "0.9.4", "@ff-labs/fff-bin-win32-x64": "0.9.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-7HUraaK/g5dStAnuKAuzsXVOQvqqX0ylo5G+DxYwsCjCDc42bjoEAAHqz/3Sn3raUNw97KMoz87XR9QyrLEfVw=="],
|
"@ff-labs/fff-bin-win32-x64": ["@ff-labs/fff-bin-win32-x64@0.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-dcpHUCBEZoXKQCdKa3bADfgcNyeyfM8tatXrILqIFYi9GL8E4GnzKx1rGkgP5ukVtuj0WuoJyyqSvGjpJPIT8w=="],
|
||||||
|
|
||||||
|
"@ff-labs/fff-bun": ["@ff-labs/fff-bun@0.10.1", "", { "optionalDependencies": { "@ff-labs/fff-bin-android-arm64": "0.10.1", "@ff-labs/fff-bin-darwin-arm64": "0.10.1", "@ff-labs/fff-bin-darwin-x64": "0.10.1", "@ff-labs/fff-bin-linux-arm64-gnu": "0.10.1", "@ff-labs/fff-bin-linux-arm64-musl": "0.10.1", "@ff-labs/fff-bin-linux-x64-gnu": "0.10.1", "@ff-labs/fff-bin-linux-x64-musl": "0.10.1", "@ff-labs/fff-bin-win32-arm64": "0.10.1", "@ff-labs/fff-bin-win32-x64": "0.10.1" }, "os": [ "!aix", "!sunos", "!freebsd", "!openbsd", ], "cpu": [ "x64", "arm64", ] }, "sha512-9oUCxypGbf2q3vNfKZ31wdzt5KqjhA9S6TwQaFol/j1lkSHVGvtIa3RvdGHPs1UUuvR/MO7b6pHj054BR9bXPQ=="],
|
||||||
|
|
||||||
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
|
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,6 @@
|
||||||
"@types/node": "catalog:"
|
"@types/node": "catalog:"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch",
|
|
||||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ const GENERATE_OBJECT_TOOL_NAME = "generate_object"
|
||||||
|
|
||||||
const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool."
|
const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool."
|
||||||
|
|
||||||
type GenerateObjectBase = Omit<RequestInput, "tools" | "toolChoice" | "responseFormat">
|
type GenerateObjectBase = Omit<RequestInput, "tools" | "toolChoice">
|
||||||
|
|
||||||
export class GenerateObjectResponse<T> {
|
export class GenerateObjectResponse<T> {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ const AnthropicTool = Schema.Struct({
|
||||||
type AnthropicTool = Schema.Schema.Type<typeof AnthropicTool>
|
type AnthropicTool = Schema.Schema.Type<typeof AnthropicTool>
|
||||||
|
|
||||||
const AnthropicToolChoice = Schema.Union([
|
const AnthropicToolChoice = Schema.Union([
|
||||||
Schema.Struct({ type: Schema.Literals(["auto", "any"]) }),
|
Schema.Struct({ type: Schema.Literals(["auto", "any", "none"]) }),
|
||||||
Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
|
Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
@ -297,7 +297,7 @@ const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition, inputSc
|
||||||
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
|
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
|
||||||
ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, {
|
ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, {
|
||||||
auto: () => ({ type: "auto" as const }),
|
auto: () => ({ type: "auto" as const }),
|
||||||
none: () => undefined,
|
none: () => ({ type: "none" as const }),
|
||||||
required: () => ({ type: "any" as const }),
|
required: () => ({ type: "any" as const }),
|
||||||
tool: (name) => ({ type: "tool" as const, name }),
|
tool: (name) => ({ type: "tool" as const, name }),
|
||||||
})
|
})
|
||||||
|
|
@ -542,7 +542,6 @@ const outputConfig = (request: LLMRequest) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
|
const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
|
||||||
const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
|
|
||||||
const generation = request.generation
|
const generation = request.generation
|
||||||
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
||||||
const outputLimit = request.model.defaults?.limits?.output ?? request.model.route.defaults.limits?.output ?? 4096
|
const outputLimit = request.model.defaults?.limits?.output ?? request.model.route.defaults.limits?.output ?? 4096
|
||||||
|
|
@ -551,7 +550,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
|
||||||
// over-mark we keep their tool hints and shed the message-tail ones first.
|
// over-mark we keep their tool hints and shed the message-tail ones first.
|
||||||
const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP)
|
const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP)
|
||||||
const tools =
|
const tools =
|
||||||
request.tools.length === 0 || request.toolChoice?.type === "none"
|
request.tools.length === 0
|
||||||
? undefined
|
? undefined
|
||||||
: request.tools.map((tool) =>
|
: request.tools.map((tool) =>
|
||||||
lowerTool(
|
lowerTool(
|
||||||
|
|
@ -560,6 +559,9 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
|
||||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
|
ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
// Anthropic rejects tool_choice when tools are absent; "none" is only meaningful with tools present.
|
||||||
|
const toolChoice =
|
||||||
|
tools === undefined || !request.toolChoice ? undefined : yield* lowerToolChoice(request.toolChoice)
|
||||||
const system =
|
const system =
|
||||||
request.system.length === 0
|
request.system.length === 0
|
||||||
? undefined
|
? undefined
|
||||||
|
|
|
||||||
|
|
@ -436,21 +436,22 @@ const mapFinishReason = (reason: string): FinishReason => {
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
// AWS Bedrock Converse reports `inputTokens` (inclusive total) with
|
// AWS reports inputTokens separately from cache reads and writes.
|
||||||
// `cacheReadInputTokens` and `cacheWriteInputTokens` as subsets. Pass
|
// Bedrock does not break reasoning out of outputTokens for current models.
|
||||||
// the total through and derive the non-cached breakdown. Bedrock does
|
|
||||||
// not break reasoning out of `outputTokens` for any current model.
|
|
||||||
const mapUsage = (usage: BedrockUsageSchema | undefined): Usage | undefined => {
|
const mapUsage = (usage: BedrockUsageSchema | undefined): Usage | undefined => {
|
||||||
if (!usage) return undefined
|
if (!usage) return undefined
|
||||||
const cacheTotal = (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0)
|
const inputTokens = ProviderShared.sumTokens(
|
||||||
const nonCached = ProviderShared.subtractTokens(usage.inputTokens, cacheTotal)
|
usage.inputTokens,
|
||||||
|
usage.cacheReadInputTokens,
|
||||||
|
usage.cacheWriteInputTokens,
|
||||||
|
)
|
||||||
return new Usage({
|
return new Usage({
|
||||||
inputTokens: usage.inputTokens,
|
inputTokens,
|
||||||
outputTokens: usage.outputTokens,
|
outputTokens: usage.outputTokens,
|
||||||
nonCachedInputTokens: nonCached,
|
nonCachedInputTokens: usage.inputTokens,
|
||||||
cacheReadInputTokens: usage.cacheReadInputTokens,
|
cacheReadInputTokens: usage.cacheReadInputTokens,
|
||||||
cacheWriteInputTokens: usage.cacheWriteInputTokens,
|
cacheWriteInputTokens: usage.cacheWriteInputTokens,
|
||||||
totalTokens: ProviderShared.totalTokens(usage.inputTokens, usage.outputTokens, usage.totalTokens),
|
totalTokens: ProviderShared.totalTokens(inputTokens, usage.outputTokens, usage.totalTokens),
|
||||||
providerMetadata: { bedrock: usage },
|
providerMetadata: { bedrock: usage },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,7 @@ const thinkingConfig = (request: LLMRequest) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMRequest) {
|
const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMRequest) {
|
||||||
const toolsEnabled = request.tools.length > 0 && request.toolChoice?.type !== "none"
|
const hasTools = request.tools.length > 0
|
||||||
const generation = request.generation
|
const generation = request.generation
|
||||||
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
||||||
const generationConfig = {
|
const generationConfig = {
|
||||||
|
|
@ -329,7 +329,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque
|
||||||
contents: yield* lowerMessages(request),
|
contents: yield* lowerMessages(request),
|
||||||
systemInstruction:
|
systemInstruction:
|
||||||
request.system.length === 0 ? undefined : { parts: [{ text: ProviderShared.joinText(request.system) }] },
|
request.system.length === 0 ? undefined : { parts: [{ text: ProviderShared.joinText(request.system) }] },
|
||||||
tools: toolsEnabled
|
tools: hasTools
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
functionDeclarations: request.tools.map((tool) =>
|
functionDeclarations: request.tools.map((tool) =>
|
||||||
|
|
@ -338,7 +338,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: undefined,
|
: undefined,
|
||||||
toolConfig: toolsEnabled && request.toolChoice ? yield* lowerToolConfig(request.toolChoice) : undefined,
|
toolConfig: hasTools && request.toolChoice ? yield* lowerToolConfig(request.toolChoice) : undefined,
|
||||||
generationConfig: Object.values(generationConfig).some((value) => value !== undefined)
|
generationConfig: Object.values(generationConfig).some((value) => value !== undefined)
|
||||||
? generationConfig
|
? generationConfig
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ const OpenAIResponsesEvent = Schema.Struct({
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
id: Schema.optional(Schema.String),
|
id: Schema.optional(Schema.String),
|
||||||
service_tier: optionalNull(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),
|
usage: optionalNull(OpenAIResponsesUsage),
|
||||||
error: optionalNull(OpenAIResponsesErrorPayload),
|
error: optionalNull(OpenAIResponsesErrorPayload),
|
||||||
}),
|
}),
|
||||||
|
|
@ -602,7 +602,8 @@ const mapUsage = (usage: OpenAIResponsesUsage | null | undefined) => {
|
||||||
|
|
||||||
const mapFinishReason = (event: OpenAIResponsesEvent, hasFunctionCall: boolean): FinishReason => {
|
const mapFinishReason = (event: OpenAIResponsesEvent, hasFunctionCall: boolean): FinishReason => {
|
||||||
const reason = event.response?.incomplete_details?.reason
|
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 === "max_output_tokens") return "length"
|
||||||
if (reason === "content_filter") return "content-filter"
|
if (reason === "content_filter") return "content-filter"
|
||||||
return hasFunctionCall ? "tool-calls" : "unknown"
|
return hasFunctionCall ? "tool-calls" : "unknown"
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,12 @@ import { ProviderFailureClassification } from "./errors"
|
||||||
*
|
*
|
||||||
* **Semantics by provider**:
|
* **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
|
* `inputTokens` and an inclusive `outputTokens`; mapper subtracts to
|
||||||
* derive the breakdown.
|
* derive the breakdown.
|
||||||
* - Anthropic: provider reports the breakdown natively (`input_tokens` is
|
* - Anthropic and Bedrock report the input breakdown natively: Anthropic's
|
||||||
* non-cached only); mapper sums to derive the inclusive `inputTokens`.
|
* `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
|
* Anthropic does *not* break extended-thinking out of `output_tokens`, so
|
||||||
* `reasoningTokens` is `undefined` and `outputTokens` carries the
|
* `reasoningTokens` is `undefined` and `outputTokens` carries the
|
||||||
* combined total — a documented limitation of the Anthropic API.
|
* combined total — a documented limitation of the Anthropic API.
|
||||||
|
|
|
||||||
|
|
@ -261,13 +261,6 @@ export namespace ToolChoice {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResponseFormat = Schema.Union([
|
|
||||||
Schema.Struct({ type: Schema.Literal("text") }),
|
|
||||||
Schema.Struct({ type: Schema.Literal("json"), schema: JsonSchema }),
|
|
||||||
Schema.Struct({ type: Schema.Literal("tool"), tool: ToolDefinition }),
|
|
||||||
]).pipe(Schema.toTaggedUnion("type"))
|
|
||||||
export type ResponseFormat = Schema.Schema.Type<typeof ResponseFormat>
|
|
||||||
|
|
||||||
export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
|
export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
|
||||||
id: Schema.optional(Schema.String),
|
id: Schema.optional(Schema.String),
|
||||||
model: ModelSchema,
|
model: ModelSchema,
|
||||||
|
|
@ -278,7 +271,6 @@ export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
|
||||||
generation: Schema.optional(GenerationOptions),
|
generation: Schema.optional(GenerationOptions),
|
||||||
providerOptions: Schema.optional(ProviderOptions),
|
providerOptions: Schema.optional(ProviderOptions),
|
||||||
http: Schema.optional(HttpOptions),
|
http: Schema.optional(HttpOptions),
|
||||||
responseFormat: Schema.optional(ResponseFormat),
|
|
||||||
cache: Schema.optional(CachePolicy),
|
cache: Schema.optional(CachePolicy),
|
||||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
@ -296,7 +288,6 @@ export namespace LLMRequest {
|
||||||
generation: request.generation,
|
generation: request.generation,
|
||||||
providerOptions: request.providerOptions,
|
providerOptions: request.providerOptions,
|
||||||
http: request.http,
|
http: request.http,
|
||||||
responseFormat: request.responseFormat,
|
|
||||||
cache: request.cache,
|
cache: request.cache,
|
||||||
metadata: request.metadata,
|
metadata: request.metadata,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -235,6 +235,34 @@ describe("Anthropic Messages route", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("keeps tools and sends tool_choice none", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||||
|
LLM.request({
|
||||||
|
id: "req_tool_choice_none",
|
||||||
|
model,
|
||||||
|
tools: [{ name: "lookup", description: "Look things up", inputSchema: { type: "object", properties: {} } }],
|
||||||
|
messages: [
|
||||||
|
Message.user("What is the weather?"),
|
||||||
|
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })]),
|
||||||
|
Message.tool({ id: "call_1", name: "lookup", result: { forecast: "sunny" } }),
|
||||||
|
],
|
||||||
|
toolChoice: "none",
|
||||||
|
cache: "none",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.tools).toEqual([
|
||||||
|
{
|
||||||
|
name: "lookup",
|
||||||
|
description: "Look things up",
|
||||||
|
input_schema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(prepared.body.tool_choice).toEqual({ type: "none" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// Regression: read tool results must stay structured so base64 media data is
|
// Regression: read tool results must stay structured so base64 media data is
|
||||||
// not JSON-stringified into `tool_result.content`.
|
// not JSON-stringified into `tool_result.content`.
|
||||||
it.effect("lowers media tool-result content as structured blocks", () =>
|
it.effect("lowers media tool-result content as structured blocks", () =>
|
||||||
|
|
|
||||||
|
|
@ -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
|
// call wouldn't deterministically prove cache mapping works. Override with
|
||||||
// BEDROCK_CACHE_MODEL_ID if your account has access elsewhere.
|
// BEDROCK_CACHE_MODEL_ID if your account has access elsewhere.
|
||||||
const model = AmazonBedrock.configure({
|
const model = AmazonBedrock.configure({
|
||||||
credentials: {
|
apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture",
|
||||||
region: RECORDING_REGION,
|
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,
|
|
||||||
},
|
|
||||||
}).model(process.env.BEDROCK_CACHE_MODEL_ID ?? "us.anthropic.claude-haiku-4-5-20251001-v1:0")
|
}).model(process.env.BEDROCK_CACHE_MODEL_ID ?? "us.anthropic.claude-haiku-4-5-20251001-v1:0")
|
||||||
|
|
||||||
const cacheRequest = LLM.request({
|
const cacheRequest = LLM.request({
|
||||||
|
|
@ -36,7 +32,7 @@ const recorded = recordedTests({
|
||||||
prefix: "bedrock-converse-cache",
|
prefix: "bedrock-converse-cache",
|
||||||
provider: "amazon-bedrock",
|
provider: "amazon-bedrock",
|
||||||
protocol: "bedrock-converse",
|
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
|
// Two identical requests in one cassette — replay walks the cassette in
|
||||||
// recording order so the second call replays the cached-hit interaction.
|
// 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"] }, () =>
|
recorded.effect.with("writes then reads cachePoint on identical second call", { tags: ["cache"] }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const first = yield* LLMClient.generate(cacheRequest)
|
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)
|
const second = yield* LLMClient.generate(cacheRequest)
|
||||||
expect(second.usage?.cacheReadInputTokens ?? 0).toBeGreaterThan(0)
|
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", () =>
|
it.effect("assembles streamed tool call input", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const body = eventStreamBody(
|
const body = eventStreamBody(
|
||||||
|
|
|
||||||
|
|
@ -233,11 +233,11 @@ describe("Gemini route", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("omits tools when tool choice is none", () =>
|
it.effect("keeps tools and sends function calling mode NONE", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const prepared = yield* LLMClient.prepare(
|
const prepared = yield* LLMClient.prepare(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
id: "req_no_tools",
|
id: "req_tool_choice_none",
|
||||||
model,
|
model,
|
||||||
prompt: "Say hello.",
|
prompt: "Say hello.",
|
||||||
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
|
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
|
||||||
|
|
@ -245,8 +245,10 @@ describe("Gemini route", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(prepared.body).toEqual({
|
expect(prepared.body).toMatchObject({
|
||||||
contents: [{ role: "user", parts: [{ text: "Say hello." }] }],
|
contents: [{ role: "user", parts: [{ text: "Say hello." }] }],
|
||||||
|
tools: [{ functionDeclarations: [{ name: "lookup", description: "Lookup data" }] }],
|
||||||
|
toolConfig: { functionCallingConfig: { mode: "NONE" } },
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
// OpenAI's documented stream orders output text within one message item; no
|
||||||
// provider-valid same-kind overlap is evidenced, so done boundaries close it.
|
// provider-valid same-kind overlap is evidenced, so done boundaries close it.
|
||||||
it.effect("closes sequential output messages before starting the next", () =>
|
it.effect("closes sequential output messages before starting the next", () =>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"typecheck": "tsgo --noEmit"
|
"typecheck": "tsgo --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "0.21.0",
|
"@agentclientprotocol/sdk": "1.2.1",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
type AuthenticateRequest,
|
type AuthenticateRequest,
|
||||||
type CancelNotification,
|
type CancelNotification,
|
||||||
type CloseSessionRequest,
|
type CloseSessionRequest,
|
||||||
|
type DeleteSessionRequest,
|
||||||
type ForkSessionRequest,
|
type ForkSessionRequest,
|
||||||
type InitializeRequest,
|
type InitializeRequest,
|
||||||
type ListSessionsRequest,
|
type ListSessionsRequest,
|
||||||
|
|
@ -13,7 +14,6 @@ import {
|
||||||
type PromptRequest,
|
type PromptRequest,
|
||||||
type ResumeSessionRequest,
|
type ResumeSessionRequest,
|
||||||
type SetSessionConfigOptionRequest,
|
type SetSessionConfigOptionRequest,
|
||||||
type SetSessionModelRequest,
|
|
||||||
type SetSessionModeRequest,
|
type SetSessionModeRequest,
|
||||||
} from "@agentclientprotocol/sdk"
|
} from "@agentclientprotocol/sdk"
|
||||||
import type { OpenCodeClient } from "@opencode-ai/client/promise"
|
import type { OpenCodeClient } from "@opencode-ai/client/promise"
|
||||||
|
|
@ -28,12 +28,12 @@ export function create(client: OpenCodeClient, connection: AgentSideConnection)
|
||||||
newSession: (params: NewSessionRequest) => run(service.newSession(params)),
|
newSession: (params: NewSessionRequest) => run(service.newSession(params)),
|
||||||
loadSession: (params: LoadSessionRequest) => run(service.loadSession(params)),
|
loadSession: (params: LoadSessionRequest) => run(service.loadSession(params)),
|
||||||
listSessions: (params: ListSessionsRequest) => run(service.listSessions(params)),
|
listSessions: (params: ListSessionsRequest) => run(service.listSessions(params)),
|
||||||
|
deleteSession: (params: DeleteSessionRequest) => run(service.deleteSession(params)),
|
||||||
resumeSession: (params: ResumeSessionRequest) => run(service.resumeSession(params)),
|
resumeSession: (params: ResumeSessionRequest) => run(service.resumeSession(params)),
|
||||||
closeSession: (params: CloseSessionRequest) => run(service.closeSession(params)),
|
closeSession: (params: CloseSessionRequest) => run(service.closeSession(params)),
|
||||||
unstable_forkSession: (params: ForkSessionRequest) => run(service.forkSession(params)),
|
unstable_forkSession: (params: ForkSessionRequest) => run(service.forkSession(params)),
|
||||||
setSessionConfigOption: (params: SetSessionConfigOptionRequest) => run(service.setSessionConfigOption(params)),
|
setSessionConfigOption: (params: SetSessionConfigOptionRequest) => run(service.setSessionConfigOption(params)),
|
||||||
setSessionMode: (params: SetSessionModeRequest) => run(service.setSessionMode(params)),
|
setSessionMode: (params: SetSessionModeRequest) => run(service.setSessionMode(params)),
|
||||||
unstable_setSessionModel: (params: SetSessionModelRequest) => run(service.setSessionModel(params)),
|
|
||||||
prompt: (params: PromptRequest) => run(service.prompt(params)),
|
prompt: (params: PromptRequest) => run(service.prompt(params)),
|
||||||
cancel: (params: CancelNotification) => run(service.cancel(params)),
|
cancel: (params: CancelNotification) => run(service.cancel(params)),
|
||||||
} satisfies Agent
|
} satisfies Agent
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export async function streamTurn(input: {
|
||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
readonly cwd: string
|
readonly cwd: string
|
||||||
readonly start: TurnStart
|
readonly start: TurnStart
|
||||||
readonly userMessageID?: string | null
|
readonly writeTextFile: boolean
|
||||||
readonly submit: (signal: AbortSignal) => Promise<unknown>
|
readonly submit: (signal: AbortSignal) => Promise<unknown>
|
||||||
readonly control: TurnControl
|
readonly control: TurnControl
|
||||||
}): Promise<PromptResponse> {
|
}): Promise<PromptResponse> {
|
||||||
|
|
@ -170,6 +170,7 @@ export async function streamTurn(input: {
|
||||||
tools.delete(event.data.callID)
|
tools.delete(event.data.callID)
|
||||||
await syncEditedFiles({
|
await syncEditedFiles({
|
||||||
connection: input.connection,
|
connection: input.connection,
|
||||||
|
writeTextFile: input.writeTextFile,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
toolName: current.name,
|
toolName: current.name,
|
||||||
|
|
@ -231,7 +232,7 @@ export async function streamTurn(input: {
|
||||||
if (!started) {
|
if (!started) {
|
||||||
streamController.abort()
|
streamController.abort()
|
||||||
await completed.catch(() => {})
|
await completed.catch(() => {})
|
||||||
return response(undefined, undefined, "interrupted", true, undefined, input.userMessageID)
|
return response(undefined, undefined, "interrupted", true, undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const terminal = await completed
|
const terminal = await completed
|
||||||
|
|
@ -246,7 +247,6 @@ export async function streamTurn(input: {
|
||||||
terminal,
|
terminal,
|
||||||
control.cancelled,
|
control.cancelled,
|
||||||
finish,
|
finish,
|
||||||
input.userMessageID,
|
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
streamController.abort()
|
streamController.abort()
|
||||||
|
|
@ -400,7 +400,6 @@ function response(
|
||||||
terminal: "succeeded" | "failed" | "interrupted",
|
terminal: "succeeded" | "failed" | "interrupted",
|
||||||
cancelled: boolean,
|
cancelled: boolean,
|
||||||
finish: SessionMessageAssistant["finish"],
|
finish: SessionMessageAssistant["finish"],
|
||||||
messageID: string | null | undefined,
|
|
||||||
): PromptResponse {
|
): PromptResponse {
|
||||||
const error = assistant?.error ?? executionError
|
const error = assistant?.error ?? executionError
|
||||||
if (error?.type === "provider.auth") throw new ACPError.AuthRequiredError()
|
if (error?.type === "provider.auth") throw new ACPError.AuthRequiredError()
|
||||||
|
|
@ -423,7 +422,7 @@ function response(
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
const stopReason = resolveStopReason({ terminal, cancelled, finish, error: error?.type })
|
const stopReason = resolveStopReason({ terminal, cancelled, finish, error: error?.type })
|
||||||
return { stopReason, ...(usage ? { usage } : {}), ...(messageID ? { userMessageId: messageID } : {}), _meta: {} }
|
return { stopReason, ...(usage ? { usage } : {}), _meta: {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveStopReason(input: {
|
function resolveStopReason(input: {
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,14 @@ export async function replyPermission(input: {
|
||||||
|
|
||||||
export async function syncEditedFiles(input: {
|
export async function syncEditedFiles(input: {
|
||||||
readonly connection: Partial<Pick<AgentSideConnection, "writeTextFile">>
|
readonly connection: Partial<Pick<AgentSideConnection, "writeTextFile">>
|
||||||
|
readonly writeTextFile: boolean
|
||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
readonly cwd: string
|
readonly cwd: string
|
||||||
readonly toolName: string
|
readonly toolName: string
|
||||||
readonly toolInput: ToolInput
|
readonly toolInput: ToolInput
|
||||||
readonly structured: Readonly<Record<string, unknown>>
|
readonly structured: Readonly<Record<string, unknown>>
|
||||||
}) {
|
}) {
|
||||||
if (!input.connection.writeTextFile || toToolKind(input.toolName) !== "edit") return
|
if (!input.writeTextFile || !input.connection.writeTextFile || toToolKind(input.toolName) !== "edit") return
|
||||||
const files = Array.isArray(input.structured.files)
|
const files = Array.isArray(input.structured.files)
|
||||||
? input.structured.files.flatMap((file): string[] => {
|
? input.structured.files.flatMap((file): string[] => {
|
||||||
if (!file || typeof file !== "object") return []
|
if (!file || typeof file !== "object") return []
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import type {
|
||||||
CancelNotification,
|
CancelNotification,
|
||||||
CloseSessionRequest,
|
CloseSessionRequest,
|
||||||
CloseSessionResponse,
|
CloseSessionResponse,
|
||||||
|
DeleteSessionRequest,
|
||||||
|
DeleteSessionResponse,
|
||||||
ForkSessionRequest,
|
ForkSessionRequest,
|
||||||
ForkSessionResponse,
|
ForkSessionResponse,
|
||||||
InitializeRequest,
|
InitializeRequest,
|
||||||
|
|
@ -33,8 +35,6 @@ import type {
|
||||||
ResumeSessionResponse,
|
ResumeSessionResponse,
|
||||||
SetSessionConfigOptionRequest,
|
SetSessionConfigOptionRequest,
|
||||||
SetSessionConfigOptionResponse,
|
SetSessionConfigOptionResponse,
|
||||||
SetSessionModelRequest,
|
|
||||||
SetSessionModelResponse,
|
|
||||||
SetSessionModeRequest,
|
SetSessionModeRequest,
|
||||||
SetSessionModeResponse,
|
SetSessionModeResponse,
|
||||||
} from "@agentclientprotocol/sdk"
|
} from "@agentclientprotocol/sdk"
|
||||||
|
|
@ -47,7 +47,8 @@ import { ACPError } from "./error"
|
||||||
|
|
||||||
export const AuthMethodID = "opencode-login"
|
export const AuthMethodID = "opencode-login"
|
||||||
|
|
||||||
type Connection = Pick<AgentSideConnection, "sessionUpdate" | "requestPermission">
|
type Connection = Pick<AgentSideConnection, "sessionUpdate" | "requestPermission"> &
|
||||||
|
Partial<Pick<AgentSideConnection, "writeTextFile">>
|
||||||
|
|
||||||
type Catalog = {
|
type Catalog = {
|
||||||
readonly providers: ConfigOptionProvider[]
|
readonly providers: ConfigOptionProvider[]
|
||||||
|
|
@ -83,12 +84,12 @@ export interface Interface {
|
||||||
newSession(input: NewSessionRequest): Promise<NewSessionResponse>
|
newSession(input: NewSessionRequest): Promise<NewSessionResponse>
|
||||||
loadSession(input: LoadSessionRequest): Promise<LoadSessionResponse>
|
loadSession(input: LoadSessionRequest): Promise<LoadSessionResponse>
|
||||||
listSessions(input: ListSessionsRequest): Promise<ListSessionsResponse>
|
listSessions(input: ListSessionsRequest): Promise<ListSessionsResponse>
|
||||||
|
deleteSession(input: DeleteSessionRequest): Promise<DeleteSessionResponse>
|
||||||
resumeSession(input: ResumeSessionRequest): Promise<ResumeSessionResponse>
|
resumeSession(input: ResumeSessionRequest): Promise<ResumeSessionResponse>
|
||||||
closeSession(input: CloseSessionRequest): Promise<CloseSessionResponse>
|
closeSession(input: CloseSessionRequest): Promise<CloseSessionResponse>
|
||||||
forkSession(input: ForkSessionRequest): Promise<ForkSessionResponse>
|
forkSession(input: ForkSessionRequest): Promise<ForkSessionResponse>
|
||||||
setSessionConfigOption(input: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse>
|
setSessionConfigOption(input: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse>
|
||||||
setSessionMode(input: SetSessionModeRequest): Promise<SetSessionModeResponse>
|
setSessionMode(input: SetSessionModeRequest): Promise<SetSessionModeResponse>
|
||||||
setSessionModel(input: SetSessionModelRequest): Promise<SetSessionModelResponse>
|
|
||||||
prompt(input: PromptRequest): Promise<PromptResponse>
|
prompt(input: PromptRequest): Promise<PromptResponse>
|
||||||
cancel(input: CancelNotification): Promise<void>
|
cancel(input: CancelNotification): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
@ -98,6 +99,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
|
||||||
const catalogs = new Map<string, Promise<Catalog>>()
|
const catalogs = new Map<string, Promise<Catalog>>()
|
||||||
const registeredMcp = new Map<string, Set<string>>()
|
const registeredMcp = new Map<string, Set<string>>()
|
||||||
const active = new Map<string, TurnControl>()
|
const active = new Map<string, TurnControl>()
|
||||||
|
const capabilities = { writeTextFile: false }
|
||||||
|
|
||||||
const catalog = (cwd: string) => {
|
const catalog = (cwd: string) => {
|
||||||
const cached = catalogs.get(cwd)
|
const cached = catalogs.get(cwd)
|
||||||
|
|
@ -157,6 +159,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialize: async (params) => {
|
initialize: async (params) => {
|
||||||
|
capabilities.writeTextFile = params.clientCapabilities?.fs?.writeTextFile === true
|
||||||
const authMethod: AuthMethod = {
|
const authMethod: AuthMethod = {
|
||||||
description: "Run `opencode auth login` in the terminal",
|
description: "Run `opencode auth login` in the terminal",
|
||||||
name: "Login with opencode",
|
name: "Login with opencode",
|
||||||
|
|
@ -173,7 +176,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
|
||||||
loadSession: true,
|
loadSession: true,
|
||||||
mcpCapabilities: { http: true, sse: false },
|
mcpCapabilities: { http: true, sse: false },
|
||||||
promptCapabilities: { embeddedContext: true, image: true },
|
promptCapabilities: { embeddedContext: true, image: true },
|
||||||
sessionCapabilities: { close: {}, fork: {}, list: {}, resume: {} },
|
sessionCapabilities: { close: {}, delete: {}, fork: {}, list: {}, resume: {} },
|
||||||
},
|
},
|
||||||
authMethods: [authMethod],
|
authMethods: [authMethod],
|
||||||
agentInfo: { name: "OpenCode", version: OPENCODE_VERSION },
|
agentInfo: { name: "OpenCode", version: OPENCODE_VERSION },
|
||||||
|
|
@ -216,6 +219,14 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
|
||||||
...(page.cursor.next ? { nextCursor: page.cursor.next } : {}),
|
...(page.cursor.next ? { nextCursor: page.cursor.next } : {}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
deleteSession: async (params) => {
|
||||||
|
await input.client.session.remove({ sessionID: params.sessionId }).catch((error) => {
|
||||||
|
if (!isSessionNotFoundError(error)) throw error
|
||||||
|
})
|
||||||
|
sessions.delete(params.sessionId)
|
||||||
|
registeredMcp.delete(params.sessionId)
|
||||||
|
return {}
|
||||||
|
},
|
||||||
resumeSession: async (params) => {
|
resumeSession: async (params) => {
|
||||||
const session = await getSession(input.client, params.sessionId)
|
const session = await getSession(input.client, params.sessionId)
|
||||||
const state = await attach(session, session.location.directory, params.mcpServers ?? [])
|
const state = await attach(session, session.location.directory, params.mcpServers ?? [])
|
||||||
|
|
@ -270,13 +281,6 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
|
||||||
await selectMode(input.client, await requireSession(params.sessionId), params.modeId)
|
await selectMode(input.client, await requireSession(params.sessionId), params.modeId)
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
setSessionModel: async (params) => {
|
|
||||||
const state = await requireSession(params.sessionId)
|
|
||||||
const selected = requireModel(state.catalog, params.modelId)
|
|
||||||
state.model = selected
|
|
||||||
await input.client.session.switchModel({ sessionID: state.id, model: selected })
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
prompt: async (params) => {
|
prompt: async (params) => {
|
||||||
const state = await requireSession(params.sessionId)
|
const state = await requireSession(params.sessionId)
|
||||||
if (active.has(state.id)) {
|
if (active.has(state.id)) {
|
||||||
|
|
@ -295,7 +299,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
|
||||||
sessionID: state.id,
|
sessionID: state.id,
|
||||||
cwd: state.cwd,
|
cwd: state.cwd,
|
||||||
start: prepared.start,
|
start: prepared.start,
|
||||||
userMessageID: params.messageId,
|
writeTextFile: capabilities.writeTextFile,
|
||||||
control,
|
control,
|
||||||
submit: (signal) => submitPrompt(input.client, state, prepared, signal),
|
submit: (signal) => submitPrompt(input.client, state, prepared, signal),
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
@ -479,6 +483,7 @@ async function registerMcpServers(
|
||||||
|
|
||||||
function mcpConfig(server: McpServer) {
|
function mcpConfig(server: McpServer) {
|
||||||
if ("type" in server) {
|
if ("type" in server) {
|
||||||
|
if (server.type === "acp") throw new Error("MCP-over-ACP is not supported")
|
||||||
return {
|
return {
|
||||||
type: "remote" as const,
|
type: "remote" as const,
|
||||||
url: server.url,
|
url: server.url,
|
||||||
|
|
|
||||||
|
|
@ -140,11 +140,11 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
||||||
Flag.withDefault(false),
|
Flag.withDefault(false),
|
||||||
),
|
),
|
||||||
replay: Flag.boolean("replay").pipe(
|
replay: Flag.boolean("replay").pipe(
|
||||||
Flag.withDescription("Replay session history on resume and after resize"),
|
Flag.withDescription("Restore session history on resume and resize (disable with --no-replay)"),
|
||||||
Flag.withDefault(true),
|
Flag.optional,
|
||||||
),
|
),
|
||||||
replayLimit: Flag.integer("replay-limit").pipe(
|
replayLimit: Flag.integer("replay-limit").pipe(
|
||||||
Flag.withDescription("Cap visible replay to the newest N messages"),
|
Flag.withDescription("Limit replay to the newest N messages (default: 200)"),
|
||||||
Flag.optional,
|
Flag.optional,
|
||||||
),
|
),
|
||||||
model: Flag.string("model").pipe(
|
model: Flag.string("model").pipe(
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ export default Runtime.handler(Commands.commands.mini, (input) =>
|
||||||
model: Option.getOrUndefined(input.model),
|
model: Option.getOrUndefined(input.model),
|
||||||
agent: Option.getOrUndefined(input.agent),
|
agent: Option.getOrUndefined(input.agent),
|
||||||
prompt: Option.getOrUndefined(input.prompt),
|
prompt: Option.getOrUndefined(input.prompt),
|
||||||
replay: input.replay,
|
replay: Option.getOrUndefined(input.replay) ?? resolved.mini?.replay ?? true,
|
||||||
replayLimit: Option.getOrUndefined(input.replayLimit),
|
replayLimit: Option.getOrUndefined(input.replayLimit) ?? resolved.mini?.replay_limit,
|
||||||
demo: input.demo,
|
demo: input.demo,
|
||||||
tuiConfig: resolved,
|
tuiConfig: resolved,
|
||||||
config: {
|
config: {
|
||||||
|
|
|
||||||
|
|
@ -439,6 +439,7 @@ describe("acp event behavior", () => {
|
||||||
sessionID: "ses_cancel",
|
sessionID: "ses_cancel",
|
||||||
cwd: "/workspace",
|
cwd: "/workspace",
|
||||||
start: { type: "input", id: "input_cancel" },
|
start: { type: "input", id: "input_cancel" },
|
||||||
|
writeTextFile: false,
|
||||||
control,
|
control,
|
||||||
submit: async (signal) => {
|
submit: async (signal) => {
|
||||||
await fixture.client.session.prompt(
|
await fixture.client.session.prompt(
|
||||||
|
|
@ -481,6 +482,7 @@ describe("acp event behavior", () => {
|
||||||
sessionID: "ses_cancel_admission",
|
sessionID: "ses_cancel_admission",
|
||||||
cwd: "/workspace",
|
cwd: "/workspace",
|
||||||
start: { type: "input", id: "input_cancel_admission" },
|
start: { type: "input", id: "input_cancel_admission" },
|
||||||
|
writeTextFile: false,
|
||||||
control,
|
control,
|
||||||
submit: (signal) =>
|
submit: (signal) =>
|
||||||
fixture.client.session.prompt(
|
fixture.client.session.prompt(
|
||||||
|
|
@ -566,7 +568,7 @@ function turn(input: {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
cwd: "/workspace",
|
cwd: "/workspace",
|
||||||
start: { type: "input", id: input.inputID },
|
start: { type: "input", id: input.inputID },
|
||||||
userMessageID: `client_${input.inputID}`,
|
writeTextFile: false,
|
||||||
control: { cancelled: false, admission: new AbortController() },
|
control: { cancelled: false, admission: new AbortController() },
|
||||||
submit: (signal) =>
|
submit: (signal) =>
|
||||||
input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inputID, text: "hello" }, { signal }),
|
input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inputID, text: "hello" }, { signal }),
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,6 @@ test("acp prompt resolves after ordered turn updates", async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const id = "msg_prompt"
|
const id = "msg_prompt"
|
||||||
const userMessageID = "client-message"
|
|
||||||
const response = await streamTurn({
|
const response = await streamTurn({
|
||||||
client,
|
client,
|
||||||
connection: {
|
connection: {
|
||||||
|
|
@ -97,7 +96,7 @@ test("acp prompt resolves after ordered turn updates", async () => {
|
||||||
sessionID: "ses_test",
|
sessionID: "ses_test",
|
||||||
cwd: "/workspace",
|
cwd: "/workspace",
|
||||||
start: { type: "input", id },
|
start: { type: "input", id },
|
||||||
userMessageID,
|
writeTextFile: false,
|
||||||
control: { cancelled: false, admission: new AbortController() },
|
control: { cancelled: false, admission: new AbortController() },
|
||||||
submit: () => client.session.prompt({ sessionID: "ses_test", id, text: "hi" }),
|
submit: () => client.session.prompt({ sessionID: "ses_test", id, text: "hi" }),
|
||||||
})
|
})
|
||||||
|
|
@ -112,7 +111,7 @@ test("acp prompt resolves after ordered turn updates", async () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
expect(response).toMatchObject({ stopReason: "end_turn", userMessageId: userMessageID, usage: { totalTokens: 2 } })
|
expect(response).toMatchObject({ stopReason: "end_turn", usage: { totalTokens: 2 } })
|
||||||
} finally {
|
} finally {
|
||||||
events?.close()
|
events?.close()
|
||||||
await server.stop(true)
|
await server.stop(true)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ describe("acp initialize/auth subprocess", () => {
|
||||||
expect(initialized.agentCapabilities?.mcpCapabilities?.sse).toBe(false)
|
expect(initialized.agentCapabilities?.mcpCapabilities?.sse).toBe(false)
|
||||||
expect(initialized.agentCapabilities?.loadSession).toBe(true)
|
expect(initialized.agentCapabilities?.loadSession).toBe(true)
|
||||||
expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({})
|
expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({})
|
||||||
|
expect(initialized.agentCapabilities?.sessionCapabilities?.delete).toEqual({})
|
||||||
expect(initialized.agentCapabilities?.sessionCapabilities?.fork).toEqual({})
|
expect(initialized.agentCapabilities?.sessionCapabilities?.fork).toEqual({})
|
||||||
expect(initialized.agentCapabilities?.sessionCapabilities?.list).toEqual({})
|
expect(initialized.agentCapabilities?.sessionCapabilities?.list).toEqual({})
|
||||||
expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({})
|
expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
CloseSessionResponse,
|
CloseSessionResponse,
|
||||||
|
DeleteSessionResponse,
|
||||||
ListSessionsResponse,
|
ListSessionsResponse,
|
||||||
LoadSessionResponse,
|
LoadSessionResponse,
|
||||||
ResumeSessionResponse,
|
ResumeSessionResponse,
|
||||||
|
|
@ -60,6 +61,20 @@ describe("acp lifecycle subprocess", () => {
|
||||||
expect(listed.sessions.some((item) => item.sessionId === session.sessionId)).toBe(true)
|
expect(listed.sessions.some((item) => item.sessionId === session.sessionId)).toBe(true)
|
||||||
}, 60_000)
|
}, 60_000)
|
||||||
|
|
||||||
|
test("delete capability and delete request", async () => {
|
||||||
|
await using fixture = await createAcpFixture()
|
||||||
|
const acp = fixture.spawn()
|
||||||
|
const initialized = await initialize(acp)
|
||||||
|
expect(initialized.agentCapabilities?.sessionCapabilities?.delete).toEqual({})
|
||||||
|
const session = await newSession(acp, fixture.home)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
expectOk(await acp.request<DeleteSessionResponse>("session/delete", { sessionId: session.sessionId })),
|
||||||
|
).toEqual({})
|
||||||
|
const listed = expectOk(await acp.request<ListSessionsResponse>("session/list", { cwd: fixture.home }))
|
||||||
|
expect(listed.sessions.some((item) => item.sessionId === session.sessionId)).toBe(false)
|
||||||
|
}, 60_000)
|
||||||
|
|
||||||
test("resume capability advertisement", async () => {
|
test("resume capability advertisement", async () => {
|
||||||
await using fixture = await createAcpFixture()
|
await using fixture = await createAcpFixture()
|
||||||
const initialized = await initialize(fixture.spawn())
|
const initialized = await initialize(fixture.spawn())
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import fs from "node:fs/promises"
|
||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { streamTurn } from "../../src/acp/event"
|
import { streamTurn } from "../../src/acp/event"
|
||||||
|
import { syncEditedFiles } from "../../src/acp/permission"
|
||||||
import { createSseFixture, durableEvent, ephemeralEvent, withTimeout } from "./sse-fixture"
|
import { createSseFixture, durableEvent, ephemeralEvent, withTimeout } from "./sse-fixture"
|
||||||
|
|
||||||
type SessionUpdateParams = Parameters<AgentSideConnection["sessionUpdate"]>[0]
|
type SessionUpdateParams = Parameters<AgentSideConnection["sessionUpdate"]>[0]
|
||||||
|
|
@ -12,6 +13,27 @@ type Connection = Pick<AgentSideConnection, "sessionUpdate" | "requestPermission
|
||||||
type Fixture = ReturnType<typeof createSseFixture>
|
type Fixture = ReturnType<typeof createSseFixture>
|
||||||
|
|
||||||
describe("acp permission behavior", () => {
|
describe("acp permission behavior", () => {
|
||||||
|
test("does not sync edits when writeTextFile was not advertised", async () => {
|
||||||
|
const writes: Parameters<AgentSideConnection["writeTextFile"]>[0][] = []
|
||||||
|
|
||||||
|
await syncEditedFiles({
|
||||||
|
connection: {
|
||||||
|
writeTextFile: async (input) => {
|
||||||
|
writes.push(input)
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
writeTextFile: false,
|
||||||
|
sessionID: "ses_no_write",
|
||||||
|
cwd: "/workspace",
|
||||||
|
toolName: "edit",
|
||||||
|
toolInput: { filePath: "/workspace/file.ts" },
|
||||||
|
structured: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(writes).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
test("forwards allow-once and allow-always selections to the generated client", async () => {
|
test("forwards allow-once and allow-always selections to the generated client", async () => {
|
||||||
const permissionRequests: RequestPermissionRequest[] = []
|
const permissionRequests: RequestPermissionRequest[] = []
|
||||||
const fixture = createSseFixture({
|
const fixture = createSseFixture({
|
||||||
|
|
@ -465,6 +487,7 @@ function startTurn(fixture: Fixture, connection: Connection, sessionID: string,
|
||||||
sessionID,
|
sessionID,
|
||||||
cwd,
|
cwd,
|
||||||
start: { type: "input", id: inputID },
|
start: { type: "input", id: inputID },
|
||||||
|
writeTextFile: true,
|
||||||
control: { cancelled: false, admission: new AbortController() },
|
control: { cancelled: false, admission: new AbortController() },
|
||||||
submit: (signal) => fixture.client.session.prompt({ sessionID, id: inputID, text: "hello" }, { signal }),
|
submit: (signal) => fixture.client.session.prompt({ sessionID, id: inputID, text: "hello" }, { signal }),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { McpServer, SessionConfigOption } from "@agentclientprotocol/sdk"
|
import type { McpServer, SessionConfigOption } from "@agentclientprotocol/sdk"
|
||||||
import { makeACPFixture, makeSession, secondModel, testModel } from "./service-fixture"
|
import { makeACPFixture, makeSession, secondModel } from "./service-fixture"
|
||||||
|
|
||||||
describe("acp service directory behavior", () => {
|
describe("acp service directory behavior", () => {
|
||||||
test("creates sessions from a catalog shared by concurrent callers in the same cwd", async () => {
|
test("creates sessions from a catalog shared by concurrent callers in the same cwd", async () => {
|
||||||
|
|
@ -134,7 +134,6 @@ describe("acp service directory behavior", () => {
|
||||||
configId: "mode",
|
configId: "mode",
|
||||||
value: "plan",
|
value: "plan",
|
||||||
})
|
})
|
||||||
await fixture.service.setSessionModel({ sessionId: session.sessionId, modelId: "test/test-model/high" })
|
|
||||||
await fixture.service.setSessionMode({ sessionId: session.sessionId, modeId: "build" })
|
await fixture.service.setSessionMode({ sessionId: session.sessionId, modeId: "build" })
|
||||||
|
|
||||||
expect(currentValue(selectedModel, "model")).toBe("test/second-model")
|
expect(currentValue(selectedModel, "model")).toBe("test/second-model")
|
||||||
|
|
@ -148,7 +147,6 @@ describe("acp service directory behavior", () => {
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{ model: { providerID: "test", id: secondModel.id } },
|
{ model: { providerID: "test", id: secondModel.id } },
|
||||||
{ model: { providerID: "test", id: secondModel.id, variant: "medium" } },
|
{ model: { providerID: "test", id: secondModel.id, variant: "medium" } },
|
||||||
{ model: { providerID: "test", id: testModel.id, variant: "high" } },
|
|
||||||
])
|
])
|
||||||
expect(
|
expect(
|
||||||
fixture.requests
|
fixture.requests
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,33 @@ describe("acp service lifecycle", () => {
|
||||||
"/api/session/missing/interrupt",
|
"/api/session/missing/interrupt",
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("deletes sessions from backing and local storage", async () => {
|
||||||
|
await using fixture = makeACPFixture({
|
||||||
|
fetch(request) {
|
||||||
|
if (request.method === "POST" && request.path === "/api/session") {
|
||||||
|
return Response.json({ data: makeSession("ses_delete") })
|
||||||
|
}
|
||||||
|
if (request.method === "DELETE" && request.path === "/api/session/ses_delete") {
|
||||||
|
return new Response(null, { status: 204 })
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const session = await fixture.service.newSession({ cwd: "/workspace", mcpServers: [] })
|
||||||
|
|
||||||
|
expect(await fixture.service.deleteSession({ sessionId: session.sessionId })).toEqual({})
|
||||||
|
expect(fixture.requests).toContainEqual({
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/api/session/ses_delete",
|
||||||
|
query: {},
|
||||||
|
body: undefined,
|
||||||
|
})
|
||||||
|
const missing = await fixture.service
|
||||||
|
.setSessionConfigOption({ sessionId: session.sessionId, configId: "effort", value: "high" })
|
||||||
|
.catch((error: unknown) => error)
|
||||||
|
expect(missing).toMatchObject({ _tag: "ACPSessionNotFoundError", sessionId: session.sessionId })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function currentValue(result: { readonly configOptions?: readonly SessionConfigOption[] | null }, id: string) {
|
function currentValue(result: { readonly configOptions?: readonly SessionConfigOption[] | null }, id: string) {
|
||||||
|
|
|
||||||
|
|
@ -45,17 +45,14 @@ describe("acp service prompt routing and usage", () => {
|
||||||
|
|
||||||
const commandResult = await fixture.service.prompt({
|
const commandResult = await fixture.service.prompt({
|
||||||
sessionId: session.sessionId,
|
sessionId: session.sessionId,
|
||||||
messageId: "client-command",
|
|
||||||
prompt: [{ type: "text", text: "/review now" }],
|
prompt: [{ type: "text", text: "/review now" }],
|
||||||
})
|
})
|
||||||
const skillResult = await fixture.service.prompt({
|
const skillResult = await fixture.service.prompt({
|
||||||
sessionId: session.sessionId,
|
sessionId: session.sessionId,
|
||||||
messageId: "client-skill",
|
|
||||||
prompt: [{ type: "text", text: "/verify" }],
|
prompt: [{ type: "text", text: "/verify" }],
|
||||||
})
|
})
|
||||||
const compactResult = await fixture.service.prompt({
|
const compactResult = await fixture.service.prompt({
|
||||||
sessionId: session.sessionId,
|
sessionId: session.sessionId,
|
||||||
messageId: "client-compact",
|
|
||||||
prompt: [{ type: "text", text: "/compact" }],
|
prompt: [{ type: "text", text: "/compact" }],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -154,13 +151,11 @@ describe("acp service prompt routing and usage", () => {
|
||||||
|
|
||||||
const response = await fixture.service.prompt({
|
const response = await fixture.service.prompt({
|
||||||
sessionId: session.sessionId,
|
sessionId: session.sessionId,
|
||||||
messageId: "client-message",
|
|
||||||
prompt: [{ type: "text", text: "hello" }],
|
prompt: [{ type: "text", text: "hello" }],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
stopReason: "end_turn",
|
stopReason: "end_turn",
|
||||||
userMessageId: "client-message",
|
|
||||||
usage: {
|
usage: {
|
||||||
inputTokens: 100,
|
inputTokens: 100,
|
||||||
outputTokens: 40,
|
outputTokens: 40,
|
||||||
|
|
|
||||||
|
|
@ -214,11 +214,15 @@ describe("mini command", () => {
|
||||||
expect(result.exitCode).toBe(0)
|
expect(result.exitCode).toBe(0)
|
||||||
expect(result.stdout).toContain("--server string")
|
expect(result.stdout).toContain("--server string")
|
||||||
expect(result.stdout).toContain("--prompt string")
|
expect(result.stdout).toContain("--prompt string")
|
||||||
|
expect(result.stdout).toContain("--replay")
|
||||||
|
expect(result.stdout).toContain("disable with --no-replay")
|
||||||
|
expect(result.stdout).toContain("--replay-limit integer")
|
||||||
|
expect(result.stdout).toContain("Limit replay to the newest N messages (default: 200)")
|
||||||
expect(result.stdout).not.toContain("SUBCOMMANDS")
|
expect(result.stdout).not.toContain("SUBCOMMANDS")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("routes local and explicit-server invocations into mini", async () => {
|
test("routes local and explicit-server invocations into mini", async () => {
|
||||||
for (const args of [["mini"], ["mini", "--server", "http://127.0.0.1:1"]]) {
|
for (const args of [["mini"], ["mini", "--no-replay"], ["mini", "--server", "http://127.0.0.1:1"]]) {
|
||||||
const result = await cli(args)
|
const result = await cli(args)
|
||||||
|
|
||||||
expect(result.exitCode).toBe(1)
|
expect(result.exitCode).toBe(1)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export type ModelRef = { id: string; providerID: string; variant?: string }
|
||||||
|
|
||||||
export type ProviderSettings = { [x: string]: JsonValue }
|
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"
|
export type PermissionV2Effect = "allow" | "deny" | "ask"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10561,26 +10561,10 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"Agent.Color": {
|
"Agent.Color": {
|
||||||
"anyOf": [
|
"type": "string",
|
||||||
|
"allOf": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"pattern": "^#[0-9a-fA-F]{6}$"
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"pattern": "^#[0-9a-fA-F]{6}$"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"primary",
|
|
||||||
"secondary",
|
|
||||||
"accent",
|
|
||||||
"success",
|
|
||||||
"warning",
|
|
||||||
"error",
|
|
||||||
"info"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
"@effect/sql-sqlite-bun": "catalog:",
|
"@effect/sql-sqlite-bun": "catalog:",
|
||||||
"@lydell/node-pty": "catalog:",
|
"@lydell/node-pty": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.29.0",
|
"@modelcontextprotocol/sdk": "1.29.0",
|
||||||
"@ff-labs/fff-bun": "0.9.4",
|
"@ff-labs/fff-bun": "0.10.1",
|
||||||
"@opencode-ai/codemode": "workspace:*",
|
"@opencode-ai/codemode": "workspace:*",
|
||||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,6 @@ function callOptions(request: LLMRequest): LanguageModelV3CallOptions {
|
||||||
presencePenalty: request.generation?.presencePenalty,
|
presencePenalty: request.generation?.presencePenalty,
|
||||||
frequencyPenalty: request.generation?.frequencyPenalty,
|
frequencyPenalty: request.generation?.frequencyPenalty,
|
||||||
seed: request.generation?.seed,
|
seed: request.generation?.seed,
|
||||||
responseFormat: responseFormat(request),
|
|
||||||
tools: request.tools.map(tool),
|
tools: request.tools.map(tool),
|
||||||
toolChoice: toolChoice(request.toolChoice),
|
toolChoice: toolChoice(request.toolChoice),
|
||||||
headers: request.http?.headers,
|
headers: request.http?.headers,
|
||||||
|
|
@ -527,12 +526,6 @@ function toolChoice(input: LLMRequest["toolChoice"]): LanguageModelV3ToolChoice
|
||||||
return { type: input.type }
|
return { type: input.type }
|
||||||
}
|
}
|
||||||
|
|
||||||
function responseFormat(request: LLMRequest): LanguageModelV3CallOptions["responseFormat"] {
|
|
||||||
if (request.responseFormat?.type === "json")
|
|
||||||
return { type: "json", schema: request.responseFormat.schema as JSONSchema7 }
|
|
||||||
if (request.responseFormat) return { type: "text" }
|
|
||||||
}
|
|
||||||
|
|
||||||
function providerOptions(input: LLMRequest["providerOptions"]): SharedV3ProviderOptions | undefined {
|
function providerOptions(input: LLMRequest["providerOptions"]): SharedV3ProviderOptions | undefined {
|
||||||
if (!input) return undefined
|
if (!input) return undefined
|
||||||
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)]))
|
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)]))
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ const layer = Layer.effect(
|
||||||
draft.providers.set(providerID, record)
|
draft.providers.set(providerID, record)
|
||||||
}
|
}
|
||||||
const model =
|
const model =
|
||||||
record.models.get(modelID) ?? (ModelV2.Info.empty(providerID, modelID) as ModelV2.MutableInfo)
|
record.models.get(modelID) ?? (ModelV2.Info.default(providerID, modelID) as ModelV2.MutableInfo)
|
||||||
if (!record.models.has(modelID)) record.models.set(modelID, model)
|
if (!record.models.has(modelID)) record.models.set(modelID, model)
|
||||||
fn(model)
|
fn(model)
|
||||||
model.id = modelID
|
model.id = modelID
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,7 @@ import { ConfigProvider } from "./provider"
|
||||||
import { ConfigModel } from "./model"
|
import { ConfigModel } from "./model"
|
||||||
import { PositiveInt } from "../schema"
|
import { PositiveInt } from "../schema"
|
||||||
|
|
||||||
export const Color = Schema.Union([
|
export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))
|
||||||
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
|
||||||
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
|
||||||
])
|
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
|
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
|
||||||
model: ConfigModel.Selection.pipe(Schema.optional),
|
model: ConfigModel.Selection.pipe(Schema.optional),
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ export * as Generate from "./generate"
|
||||||
|
|
||||||
import { LLM, LLMClient, LLMError } from "@opencode-ai/ai"
|
import { LLM, LLMClient, LLMError } from "@opencode-ai/ai"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Catalog } from "./catalog"
|
|
||||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||||
import { llmClient } from "./effect/app-node-platform"
|
import { llmClient } from "./effect/app-node-platform"
|
||||||
import { Integration } from "./integration"
|
import { ModelResolver } from "./model-resolver"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
import { SessionRunnerModel } from "./session/runner/model"
|
|
||||||
|
|
||||||
export interface TextInput {
|
export interface TextInput {
|
||||||
readonly prompt: string
|
readonly prompt: string
|
||||||
|
|
@ -19,10 +17,10 @@ export class ModelSelectionError extends Schema.TaggedErrorClass<ModelSelectionE
|
||||||
{ message: Schema.String },
|
{ message: Schema.String },
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
export class UnavailableError extends Schema.TaggedErrorClass<UnavailableError>()(
|
export class UnavailableError extends Schema.TaggedErrorClass<UnavailableError>()("Generate.UnavailableError", {
|
||||||
"Generate.UnavailableError",
|
message: Schema.String,
|
||||||
{ message: Schema.String, service: Schema.optional(Schema.String) },
|
service: Schema.optional(Schema.String),
|
||||||
) {}
|
}) {}
|
||||||
|
|
||||||
export type Error = ModelSelectionError | UnavailableError
|
export type Error = ModelSelectionError | UnavailableError
|
||||||
|
|
||||||
|
|
@ -35,56 +33,34 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
|
||||||
const integrations = yield* Integration.Service
|
|
||||||
const llm = yield* LLMClient.Service
|
const llm = yield* LLMClient.Service
|
||||||
|
const resolver = yield* ModelResolver.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 runText = Effect.fn("Generate.text")(function* (input: TextInput) {
|
const runText = Effect.fn("Generate.text")(function* (input: TextInput) {
|
||||||
const selected = yield* selectModel(input.model)
|
const resolved = yield* resolver.resolve(input.model).pipe(
|
||||||
const provider = yield* catalog.provider.get(selected.providerID)
|
Effect.catchTags({
|
||||||
const connection = yield* integrations.connection.active(
|
"SessionRunnerModel.VariantUnavailableError": (error) =>
|
||||||
provider?.integrationID ?? Integration.ID.make(selected.providerID),
|
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
|
if (!resolved)
|
||||||
const model = yield* SessionRunnerModel.fromCatalogModel(selected, credential).pipe(
|
return yield* new ModelSelectionError({
|
||||||
Effect.mapError((error) =>
|
message: input.model
|
||||||
input.model
|
? `Model unavailable: ${input.model.providerID}/${input.model.id}`
|
||||||
? new ModelSelectionError({ message: error.message })
|
: "No model specified and no supported model is available",
|
||||||
: new UnavailableError({ message: error.message, service: selected.providerID }),
|
})
|
||||||
),
|
const response = yield* llm.generate(LLM.request({ model: resolved.model, prompt: input.prompt })).pipe(
|
||||||
)
|
|
||||||
const response = yield* llm.generate(LLM.request({ model, prompt: input.prompt })).pipe(
|
|
||||||
Effect.mapError(
|
Effect.mapError(
|
||||||
(error: LLMError) =>
|
(error: LLMError) =>
|
||||||
new UnavailableError({
|
new UnavailableError({
|
||||||
message: error.message,
|
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],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?:
|
||||||
const released = previous?.time.released || Date.parse(version)
|
const released = previous?.time.released || Date.parse(version)
|
||||||
|
|
||||||
return ModelV2.Info.make({
|
return ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, id),
|
...ModelV2.Info.default(ProviderV2.ID.githubCopilot, id),
|
||||||
id,
|
id,
|
||||||
modelID: ModelV2.ID.make(remote.id),
|
modelID: ModelV2.ID.make(remote.id),
|
||||||
providerID: ProviderV2.ID.githubCopilot,
|
providerID: ProviderV2.ID.githubCopilot,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { Integration } from "./integration"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { LocationMutation } from "./location-mutation"
|
import { LocationMutation } from "./location-mutation"
|
||||||
import { LocationServiceMap } from "./location-service-map"
|
import { LocationServiceMap } from "./location-service-map"
|
||||||
|
import { ModelResolver } from "./model-resolver"
|
||||||
import { MCP } from "./mcp/index"
|
import { MCP } from "./mcp/index"
|
||||||
import { PermissionV2 } from "./permission"
|
import { PermissionV2 } from "./permission"
|
||||||
import { PluginV2 } from "./plugin"
|
import { PluginV2 } from "./plugin"
|
||||||
|
|
@ -58,6 +59,7 @@ const locationServiceNodes = [
|
||||||
Reference.node,
|
Reference.node,
|
||||||
Integration.node,
|
Integration.node,
|
||||||
Catalog.node,
|
Catalog.node,
|
||||||
|
ModelResolver.node,
|
||||||
AISDK.node,
|
AISDK.node,
|
||||||
PluginV2.node,
|
PluginV2.node,
|
||||||
PluginSupervisor.node,
|
PluginSupervisor.node,
|
||||||
|
|
|
||||||
344
packages/core/src/model-resolver.ts
Normal file
344
packages/core/src/model-resolver.ts
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
export * as ModelResolver from "./model-resolver"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||||
|
import { Model } from "@opencode-ai/ai"
|
||||||
|
// ast-grep-ignore: no-star-import
|
||||||
|
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
|
||||||
|
// ast-grep-ignore: no-star-import
|
||||||
|
import * as OpenAICompatibleChat from "@opencode-ai/ai/protocols/openai-compatible-chat"
|
||||||
|
// ast-grep-ignore: no-star-import
|
||||||
|
import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses"
|
||||||
|
import { Auth, type AnyRoute } from "@opencode-ai/ai/route"
|
||||||
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
|
import { produce } from "immer"
|
||||||
|
import { AISDK } from "./aisdk"
|
||||||
|
import { Catalog } from "./catalog"
|
||||||
|
import { Credential } from "./credential"
|
||||||
|
import { Integration } from "./integration"
|
||||||
|
import { ModelV2 } from "./model"
|
||||||
|
import { Npm } from "@opencode-ai/util/npm"
|
||||||
|
import { OpenAICodex } from "./plugin/provider/openai-codex"
|
||||||
|
import { ProviderV2 } from "./provider"
|
||||||
|
|
||||||
|
export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()(
|
||||||
|
"SessionRunnerModel.VariantUnavailableError",
|
||||||
|
{
|
||||||
|
providerID: ProviderV2.ID,
|
||||||
|
modelID: ModelV2.ID,
|
||||||
|
variant: ModelV2.VariantID,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
override get message() {
|
||||||
|
return `Variant unavailable for ${this.providerID}/${this.modelID}: ${this.variant}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
|
||||||
|
"SessionRunnerModel.UnsupportedPackageError",
|
||||||
|
{
|
||||||
|
providerID: ProviderV2.ID,
|
||||||
|
modelID: ModelV2.ID,
|
||||||
|
package: Schema.String,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
override get message() {
|
||||||
|
return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Error = VariantUnavailableError | UnsupportedPackageError | Integration.AuthorizationError
|
||||||
|
|
||||||
|
export interface Resolved {
|
||||||
|
/** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */
|
||||||
|
readonly model: Model
|
||||||
|
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
|
||||||
|
readonly ref: ModelV2.Ref
|
||||||
|
/** Catalog capabilities used to shape requests before provider lowering. */
|
||||||
|
readonly capabilities: ModelV2.Capabilities
|
||||||
|
/** Catalog pricing in dollars per million tokens. */
|
||||||
|
readonly cost: ModelV2.Info["cost"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
readonly resolve: (requested?: ModelV2.Ref) => Effect.Effect<Resolved | undefined, Error>
|
||||||
|
readonly resolveModel: (model: ModelV2.Info, variant?: ModelV2.VariantID) => Effect.Effect<Resolved, Error>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ModelResolver") {}
|
||||||
|
|
||||||
|
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
||||||
|
if (credential?.type === "key") return Auth.value(credential.key)
|
||||||
|
if (credential?.type === "oauth") return Auth.value(credential.access)
|
||||||
|
const value = model.settings?.apiKey
|
||||||
|
if (typeof value === "string") return Auth.value(value)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
|
||||||
|
route.with({
|
||||||
|
provider: model.providerID,
|
||||||
|
endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
|
||||||
|
headers: providerHeaders(model),
|
||||||
|
providerOptions: providerOptions(model),
|
||||||
|
http: model.body === undefined ? undefined : { body: model.body },
|
||||||
|
limits: { context: model.limit.context, output: model.limit.output },
|
||||||
|
})
|
||||||
|
|
||||||
|
const providerHeaders = (model: ModelV2.Info) => {
|
||||||
|
const packageName = ProviderV2.packageName(model.package)
|
||||||
|
const generated = new Map<string, string>()
|
||||||
|
if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string")
|
||||||
|
generated.set("OpenAI-Organization", model.settings.organization)
|
||||||
|
if (packageName === "@ai-sdk/openai" && typeof model.settings?.project === "string")
|
||||||
|
generated.set("OpenAI-Project", model.settings.project)
|
||||||
|
if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string")
|
||||||
|
generated.set("Authorization", `Bearer ${model.settings.authToken}`)
|
||||||
|
return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerOptions = (
|
||||||
|
model: ModelV2.Info,
|
||||||
|
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
|
||||||
|
if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined
|
||||||
|
const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings
|
||||||
|
if (Object.keys(settings).length === 0) return undefined
|
||||||
|
const packageName = ProviderV2.packageName(model.package)
|
||||||
|
if (packageName === "@ai-sdk/openai") return { openai: settings }
|
||||||
|
if (packageName === "@ai-sdk/anthropic") return { anthropic: settings }
|
||||||
|
if (packageName === "@ai-sdk/openai-compatible") return { openai: settings }
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withVariant = (
|
||||||
|
model: ModelV2.Info,
|
||||||
|
variantID: ModelV2.VariantID | undefined,
|
||||||
|
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
|
||||||
|
const id = variantID === "default" ? undefined : variantID
|
||||||
|
const variant = model.variants?.find((item) => item.id === id)
|
||||||
|
if (!variant && variantID !== undefined && variantID !== "default")
|
||||||
|
return Effect.fail(
|
||||||
|
new VariantUnavailableError({
|
||||||
|
providerID: model.providerID,
|
||||||
|
modelID: model.id,
|
||||||
|
variant: variantID,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return Effect.succeed(
|
||||||
|
variant
|
||||||
|
? produce(model, (draft) => {
|
||||||
|
draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings)
|
||||||
|
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
|
||||||
|
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
|
||||||
|
})
|
||||||
|
: model,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Dependencies {
|
||||||
|
readonly loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError>
|
||||||
|
readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect<Model, AISDK.InitError>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fromCatalogModel = (
|
||||||
|
model: ModelV2.Info,
|
||||||
|
credential?: Credential.Value,
|
||||||
|
dependencies?: Dependencies,
|
||||||
|
): Effect.Effect<Model, UnsupportedPackageError> => {
|
||||||
|
const resolved = produce(model, (draft) => {
|
||||||
|
if (draft.settings?.apiKey === "") delete draft.settings.apiKey
|
||||||
|
if (credential?.type === "key" && credential.metadata !== undefined)
|
||||||
|
draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata)
|
||||||
|
})
|
||||||
|
const packageName = ProviderV2.packageName(resolved.package)
|
||||||
|
const key = apiKey(resolved, credential)
|
||||||
|
|
||||||
|
if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
|
||||||
|
return Effect.succeed(codexModel(resolved, credential, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
|
||||||
|
if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
|
||||||
|
return Effect.succeed(
|
||||||
|
withDefaults(resolved, OpenAIResponses.route)
|
||||||
|
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||||
|
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
|
||||||
|
return Effect.succeed(
|
||||||
|
withDefaults(resolved, AnthropicMessages.route)
|
||||||
|
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
|
||||||
|
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
ProviderV2.isAISDK(resolved.package) &&
|
||||||
|
packageName === "@ai-sdk/openai-compatible" &&
|
||||||
|
typeof resolved.settings?.baseURL === "string"
|
||||||
|
) {
|
||||||
|
return Effect.succeed(
|
||||||
|
withDefaults(resolved, OpenAICompatibleChat.route)
|
||||||
|
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||||
|
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (ProviderV2.isAISDK(resolved.package)) {
|
||||||
|
if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved))
|
||||||
|
const runtime = produce(resolved, (draft) => {
|
||||||
|
draft.settings = ProviderV2.mergeOverlay(draft.settings, {
|
||||||
|
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
|
||||||
|
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
|
||||||
|
...credential?.metadata,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return dependencies.loadAISDK(runtime).pipe(Effect.mapError(() => unsupported(resolved)))
|
||||||
|
}
|
||||||
|
if (!resolved.package) return Effect.fail(unsupported(resolved))
|
||||||
|
|
||||||
|
const specifier = resolved.package
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const module = yield* (dependencies?.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe(
|
||||||
|
Effect.mapError(() => unsupported(resolved)),
|
||||||
|
)
|
||||||
|
const configured = { ...resolved.settings, ...credential?.metadata }
|
||||||
|
const settings = {
|
||||||
|
...(credential ? withoutNativeAuthSettings(configured) : configured),
|
||||||
|
...nativeCredentialSettings(specifier, credential),
|
||||||
|
headers: resolved.headers,
|
||||||
|
body: resolved.body,
|
||||||
|
limits: { context: resolved.limit.context, output: resolved.limit.output },
|
||||||
|
}
|
||||||
|
return yield* Effect.try({
|
||||||
|
try: () => {
|
||||||
|
const runtime = module.model(resolved.modelID ?? resolved.id, settings)
|
||||||
|
return Model.update(runtime, {
|
||||||
|
provider: resolved.providerID,
|
||||||
|
compatibility: resolved.compatibility
|
||||||
|
? Object.assign({}, runtime.compatibility, resolved.compatibility)
|
||||||
|
: runtime.compatibility,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
catch: () => unsupported(resolved),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNativeOpenAI = (packageName: string | undefined) =>
|
||||||
|
packageName === "@opencode-ai/ai/providers/openai" ||
|
||||||
|
packageName?.startsWith("@opencode-ai/ai/providers/openai/") === true
|
||||||
|
|
||||||
|
const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => {
|
||||||
|
if (!credential) return {}
|
||||||
|
if (credential.type === "key") return { apiKey: credential.key }
|
||||||
|
if (
|
||||||
|
specifier === "@opencode-ai/ai/providers/anthropic" ||
|
||||||
|
specifier === "@opencode-ai/ai/providers/anthropic-compatible"
|
||||||
|
)
|
||||||
|
return { authToken: credential.access }
|
||||||
|
if (
|
||||||
|
specifier === "@opencode-ai/ai/providers/google-vertex" ||
|
||||||
|
specifier.startsWith("@opencode-ai/ai/providers/google-vertex/")
|
||||||
|
)
|
||||||
|
return { accessToken: credential.access }
|
||||||
|
return { apiKey: credential.access }
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutNativeAuthSettings = (settings: Record<string, unknown>) => {
|
||||||
|
const { accessToken: _accessToken, apiKey: _apiKey, authToken: _authToken, ...rest } = settings
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
|
||||||
|
const codexModel = (
|
||||||
|
model: ModelV2.Info,
|
||||||
|
credential: Credential.Value | undefined,
|
||||||
|
key: ReturnType<typeof Auth.value> | undefined,
|
||||||
|
) => {
|
||||||
|
const account = OpenAICodex.accountID(credential)
|
||||||
|
return withDefaults(model, OpenAIResponses.route)
|
||||||
|
.with({
|
||||||
|
endpoint: { baseURL: OpenAICodex.baseURL },
|
||||||
|
auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen(
|
||||||
|
account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.model({ id: model.modelID ?? model.id, compatibility: model.compatibility })
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsupported = (model: ModelV2.Info) =>
|
||||||
|
new UnsupportedPackageError({
|
||||||
|
providerID: model.providerID,
|
||||||
|
modelID: model.id,
|
||||||
|
package: model.package ?? "unknown",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const resolveModel = (
|
||||||
|
model: ModelV2.Info,
|
||||||
|
variant: ModelV2.VariantID | undefined,
|
||||||
|
credential?: Credential.Value,
|
||||||
|
dependencies?: Dependencies,
|
||||||
|
) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)))
|
||||||
|
|
||||||
|
export const supported = (model: ModelV2.Info) => Boolean(model.package)
|
||||||
|
|
||||||
|
/** Resolves catalog selections into runtime models for the current Location. */
|
||||||
|
export const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
const npm = yield* Npm.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
|
const load = Effect.fn("ModelResolver.resolveModel")(function* (
|
||||||
|
selected: ModelV2.Info,
|
||||||
|
variant?: ModelV2.VariantID,
|
||||||
|
) {
|
||||||
|
const provider = yield* catalog.provider.get(selected.providerID)
|
||||||
|
const connection = yield* integrations.connection.active(
|
||||||
|
provider?.integrationID ?? Integration.ID.make(selected.providerID),
|
||||||
|
)
|
||||||
|
const model = yield* resolveModel(
|
||||||
|
selected,
|
||||||
|
variant,
|
||||||
|
connection ? yield* integrations.connection.resolve(connection) : undefined,
|
||||||
|
{
|
||||||
|
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
|
||||||
|
loadAISDK: (model) => aisdk.model(model),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
model,
|
||||||
|
ref: ModelV2.Ref.make({
|
||||||
|
id: selected.id,
|
||||||
|
providerID: selected.providerID,
|
||||||
|
...(variant === undefined ? {} : { variant }),
|
||||||
|
}),
|
||||||
|
capabilities: selected.capabilities,
|
||||||
|
cost: selected.cost,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Service.of({
|
||||||
|
resolve: Effect.fn("ModelResolver.resolve")(function* (requested) {
|
||||||
|
const selected = requested
|
||||||
|
? yield* catalog.model.get(requested.providerID, requested.id)
|
||||||
|
: yield* catalog.model
|
||||||
|
.default()
|
||||||
|
.pipe(
|
||||||
|
Effect.flatMap((model) =>
|
||||||
|
model && supported(model)
|
||||||
|
? Effect.succeed(model)
|
||||||
|
: Effect.map(catalog.model.available(), (models) => models.find(supported)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (!selected) return undefined
|
||||||
|
return yield* load(selected, requested?.variant)
|
||||||
|
}),
|
||||||
|
resolveModel: load,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [Catalog.node, Integration.node, Npm.node, AISDK.node],
|
||||||
|
})
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export * as OpenAICodex from "./openai-codex"
|
export * as OpenAICodex from "./openai-codex"
|
||||||
|
|
||||||
// TEMPORARY SEAM (#34765): plugins have no hook into LLM route construction, so
|
// 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
|
// in OpenAIPlugin, sharing this module. Once the native provider packages land
|
||||||
// (#33689/#33925/#34462) this should collapse into the native OpenAI provider.
|
// (#33689/#33925/#34462) this should collapse into the native OpenAI provider.
|
||||||
// The eligibility rules mirror V1's CodexAuthPlugin allowlist; models.dev has no
|
// The eligibility rules mirror V1's CodexAuthPlugin allowlist; models.dev has no
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,6 @@ export const layer = Layer.effect(
|
||||||
system: contextEvent.system,
|
system: contextEvent.system,
|
||||||
messages: contextEvent.messages,
|
messages: contextEvent.messages,
|
||||||
tools: hookedTools,
|
tools: hookedTools,
|
||||||
toolChoice: "none",
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* Effect.logInfo("session generation usage diagnostic", { usage: response.usage })
|
yield* Effect.logInfo("session generation usage diagnostic", { usage: response.usage })
|
||||||
|
|
|
||||||
|
|
@ -2,30 +2,16 @@ export * as SessionRunnerModel from "./model"
|
||||||
|
|
||||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||||
import { Model } from "@opencode-ai/ai"
|
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 { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { produce } from "immer"
|
|
||||||
import { AISDK } from "../../aisdk"
|
|
||||||
import { Catalog } from "../../catalog"
|
import { Catalog } from "../../catalog"
|
||||||
import { Credential } from "../../credential"
|
import { ModelResolver } from "../../model-resolver"
|
||||||
import { Integration } from "../../integration"
|
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { Npm } from "@opencode-ai/util/npm"
|
|
||||||
import { OpenAICodex } from "../../plugin/provider/openai-codex"
|
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
import { SessionSchema } from "../schema"
|
import { SessionSchema } from "../schema"
|
||||||
|
|
||||||
export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelectedError>()(
|
export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelectedError>()(
|
||||||
"SessionRunnerModel.ModelNotSelectedError",
|
"SessionRunnerModel.ModelNotSelectedError",
|
||||||
{
|
{ sessionID: SessionSchema.ID },
|
||||||
sessionID: SessionSchema.ID,
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
override get message() {
|
override get message() {
|
||||||
return `No model is available for session ${this.sessionID}`
|
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>()(
|
export class ModelUnavailableError extends Schema.TaggedErrorClass<ModelUnavailableError>()(
|
||||||
"SessionRunnerModel.ModelUnavailableError",
|
"SessionRunnerModel.ModelUnavailableError",
|
||||||
{
|
{ providerID: ProviderV2.ID, modelID: ModelV2.ID },
|
||||||
providerID: ProviderV2.ID,
|
|
||||||
modelID: ModelV2.ID,
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
override get message() {
|
override get message() {
|
||||||
return `Model unavailable: ${this.providerID}/${this.modelID}`
|
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>()(
|
export type Error = ModelNotSelectedError | ModelUnavailableError | ModelResolver.Error
|
||||||
"SessionRunnerModel.VariantUnavailableError",
|
export type Resolved = ModelResolver.Resolved
|
||||||
{
|
|
||||||
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 interface Interface {
|
export interface Interface {
|
||||||
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Resolved, Error>
|
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") {}
|
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. */
|
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
|
||||||
export const resolved = (
|
export const resolved = (
|
||||||
model: Model,
|
model: Model,
|
||||||
|
|
@ -116,276 +59,31 @@ export const resolved = (
|
||||||
cost: options.cost,
|
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(
|
const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const integrations = yield* Integration.Service
|
const resolver = yield* ModelResolver.Service
|
||||||
const npm = yield* Npm.Service
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
||||||
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
||||||
const defaultModel = session.model ? undefined : yield* catalog.model.default()
|
if (!session.model) {
|
||||||
const selected = session.model
|
const resolved = yield* resolver.resolve()
|
||||||
? (yield* catalog.model.available()).find(
|
if (resolved) return resolved
|
||||||
(model) => model.providerID === session.model?.providerID && model.id === session.model.id,
|
return yield* new ModelNotSelectedError({ sessionID: session.id })
|
||||||
)
|
}
|
||||||
: defaultModel && supported(defaultModel)
|
const selected = (yield* catalog.model.available()).find(
|
||||||
? defaultModel
|
(model) => model.providerID === session.model?.providerID && model.id === session.model.id,
|
||||||
: (yield* catalog.model.available()).find(supported)
|
)
|
||||||
if (!selected && session.model)
|
if (!selected)
|
||||||
return yield* new ModelUnavailableError({
|
return yield* new ModelUnavailableError({
|
||||||
providerID: session.model.providerID,
|
providerID: session.model.providerID,
|
||||||
modelID: session.model.id,
|
modelID: session.model.id,
|
||||||
})
|
})
|
||||||
if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id })
|
return yield* resolver.resolveModel(selected, session.model.variant)
|
||||||
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,
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const node = makeLocationNode({
|
export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, ModelResolver.node] })
|
||||||
service: Service,
|
|
||||||
layer,
|
|
||||||
deps: [Catalog.node, Integration.node, Npm.node, AISDK.node],
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ export function migrateAgent(info: ConfigAgentV1.Info) {
|
||||||
description: info.description,
|
description: info.description,
|
||||||
mode: info.mode,
|
mode: info.mode,
|
||||||
hidden: info.hidden,
|
hidden: info.hidden,
|
||||||
color: info.color,
|
color: info.color === undefined ? undefined : info.color.startsWith("#") ? info.color : "#aaaaaa",
|
||||||
steps: info.steps,
|
steps: info.steps,
|
||||||
disabled: info.disable,
|
disabled: info.disable,
|
||||||
permissions: permissions(info.permission),
|
permissions: permissions(info.permission),
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const it = testEffect(AISDK.locationLayer)
|
||||||
|
|
||||||
const model = (packageName: string, settings: Record<string, unknown> = {}) =>
|
const model = (packageName: string, settings: Record<string, unknown> = {}) =>
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")),
|
||||||
modelID: ModelV2.ID.make("api-model"),
|
modelID: ModelV2.ID.make("api-model"),
|
||||||
package: ProviderV2.aisdk(packageName),
|
package: ProviderV2.aisdk(packageName),
|
||||||
settings,
|
settings,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Schema } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
|
|
@ -23,6 +23,10 @@ const defaultPermissions = [
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
{ action: "external_directory", resource: "*", effect: "ask" },
|
||||||
] satisfies PermissionV2.Ruleset
|
] satisfies PermissionV2.Ruleset
|
||||||
|
|
||||||
|
test("rejects named agent color tokens", () => {
|
||||||
|
expect(() => decode({ agents: { reviewer: { color: "warning" } } })).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
describe("ConfigAgentPlugin.Plugin", () => {
|
describe("ConfigAgentPlugin.Plugin", () => {
|
||||||
it.effect("matches POSIX paths against home-relative permissions", () =>
|
it.effect("matches POSIX paths against home-relative permissions", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -160,7 +164,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
||||||
description: "Reviews changes",
|
description: "Reviews changes",
|
||||||
mode: "subagent",
|
mode: "subagent",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
color: "warning",
|
color: "#ff6b6b",
|
||||||
steps: 12,
|
steps: 12,
|
||||||
request: {
|
request: {
|
||||||
headers: { first: "one", shared: "first" },
|
headers: { first: "one", shared: "first" },
|
||||||
|
|
@ -197,7 +201,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
||||||
description: "Reviews changes",
|
description: "Reviews changes",
|
||||||
mode: "subagent",
|
mode: "subagent",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
color: "warning",
|
color: "#ff6b6b",
|
||||||
steps: 12,
|
steps: 12,
|
||||||
model: { providerID: "anthropic", id: "claude-sonnet" },
|
model: { providerID: "anthropic", id: "claude-sonnet" },
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -738,7 +738,7 @@ describe("Config", () => {
|
||||||
system: "Find regressions.",
|
system: "Find regressions.",
|
||||||
mode: "subagent",
|
mode: "subagent",
|
||||||
hidden: false,
|
hidden: false,
|
||||||
color: "warning",
|
color: "#ff6b6b",
|
||||||
steps: 12,
|
steps: 12,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
permissions: [{ action: "edit", resource: "*", effect: "deny" }],
|
permissions: [{ action: "edit", resource: "*", effect: "deny" }],
|
||||||
|
|
@ -824,7 +824,7 @@ describe("Config", () => {
|
||||||
expect(reviewer?.system).toBe("Find regressions.")
|
expect(reviewer?.system).toBe("Find regressions.")
|
||||||
expect(reviewer?.mode).toBe("subagent")
|
expect(reviewer?.mode).toBe("subagent")
|
||||||
expect(reviewer?.hidden).toBe(false)
|
expect(reviewer?.hidden).toBe(false)
|
||||||
expect(reviewer?.color).toBe("warning")
|
expect(reviewer?.color).toBe("#ff6b6b")
|
||||||
expect(reviewer?.steps).toBe(12)
|
expect(reviewer?.steps).toBe(12)
|
||||||
expect(reviewer?.disabled).toBe(false)
|
expect(reviewer?.disabled).toBe(false)
|
||||||
expect(reviewer?.permissions).toEqual([{ action: "edit", resource: "*", effect: "deny" }])
|
expect(reviewer?.permissions).toEqual([{ action: "edit", resource: "*", effect: "deny" }])
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,86 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
|
||||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
|
|
||||||
describe("ConfigProviderPlugin.Plugin", () => {
|
describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
|
it.effect("defaults custom models to agent capabilities", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const providerID = ProviderV2.ID.make("custom")
|
||||||
|
const modelID = ModelV2.ID.make("chat")
|
||||||
|
const config = Config.Service.of({
|
||||||
|
entries: () =>
|
||||||
|
Effect.succeed([
|
||||||
|
new Config.Document({
|
||||||
|
type: "document",
|
||||||
|
info: decode({
|
||||||
|
providers: {
|
||||||
|
custom: {
|
||||||
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
|
models: { chat: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* addPlugin(config)
|
||||||
|
|
||||||
|
const model = required(yield* catalog.model.get(providerID, modelID))
|
||||||
|
expect(model.capabilities).toEqual({ tools: true, input: ["text", "image"], output: ["text"] })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("preserves catalog capabilities unless config overrides them", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const providerID = ProviderV2.ID.make("custom")
|
||||||
|
const inheritedID = ModelV2.ID.make("inherited")
|
||||||
|
const overriddenID = ModelV2.ID.make("overridden")
|
||||||
|
yield* catalog.transform((draft) => {
|
||||||
|
draft.model.update(providerID, inheritedID, (model) => {
|
||||||
|
model.capabilities = { tools: false, input: ["text"], output: ["text"] }
|
||||||
|
})
|
||||||
|
draft.model.update(providerID, overriddenID, (model) => {
|
||||||
|
model.capabilities = { tools: false, input: ["text"], output: ["text"] }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const config = Config.Service.of({
|
||||||
|
entries: () =>
|
||||||
|
Effect.succeed([
|
||||||
|
new Config.Document({
|
||||||
|
type: "document",
|
||||||
|
info: decode({
|
||||||
|
providers: {
|
||||||
|
custom: {
|
||||||
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
|
models: {
|
||||||
|
inherited: { name: "Inherited" },
|
||||||
|
overridden: {
|
||||||
|
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* addPlugin(config)
|
||||||
|
|
||||||
|
expect((yield* catalog.model.get(providerID, inheritedID))?.capabilities).toEqual({
|
||||||
|
tools: false,
|
||||||
|
input: ["text"],
|
||||||
|
output: ["text"],
|
||||||
|
})
|
||||||
|
expect((yield* catalog.model.get(providerID, overriddenID))?.capabilities).toEqual({
|
||||||
|
tools: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
output: ["text"],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("keeps configured model variant bodies unchanged", () =>
|
it.effect("keeps configured model variant bodies unchanged", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
|
|
|
||||||
111
packages/core/test/generate.test.ts
Normal file
111
packages/core/test/generate.test.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { expect } from "bun:test"
|
||||||
|
import { LLMClient, LLMEvent, LLMResponse, Model } from "@opencode-ai/ai"
|
||||||
|
import { OpenAIChat } from "@opencode-ai/ai/protocols"
|
||||||
|
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||||
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
|
import { Generate } from "@opencode-ai/core/generate"
|
||||||
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
|
import { ModelResolver } from "@opencode-ai/core/model-resolver"
|
||||||
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
|
import { Npm } from "@opencode-ai/util/npm"
|
||||||
|
import { Effect, Layer, Stream } from "effect"
|
||||||
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
|
const selected = ModelV2.Info.make({
|
||||||
|
...ModelV2.Info.default(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,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
@ -49,12 +49,12 @@ test("defensively syncs advertised Copilot models", async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existing = ModelV2.Info.make({
|
const existing = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
name: "GPT-5 local",
|
name: "GPT-5 local",
|
||||||
})
|
})
|
||||||
const stale = ModelV2.Info.make({
|
const stale = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale")),
|
...ModelV2.Info.default(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale")),
|
||||||
modelID: ModelV2.ID.make("stale"),
|
modelID: ModelV2.ID.make("stale"),
|
||||||
})
|
})
|
||||||
const models = await CopilotModels.get(server.url.origin, {}, [existing, stale])
|
const models = await CopilotModels.get(server.url.origin, {}, [existing, stale])
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { LLM, Model } from "@opencode-ai/ai"
|
import { LLM, Model } from "@opencode-ai/ai"
|
||||||
import { LLMClient } from "@opencode-ai/ai/route"
|
import { LLMClient } from "@opencode-ai/ai/route"
|
||||||
import { DateTime, Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Money } from "@opencode-ai/schema/money"
|
|
||||||
import { Headers } from "effect/unstable/http"
|
import { Headers } from "effect/unstable/http"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
import { ModelResolver } from "@opencode-ai/core/model-resolver"
|
||||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
|
||||||
import { it } from "./lib/effect"
|
import { it } from "./lib/effect"
|
||||||
|
|
||||||
interface ModelOptions {
|
interface ModelOptions {
|
||||||
|
|
@ -43,13 +39,13 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) =>
|
||||||
limit: { context: 100, output: 20 },
|
limit: { context: 100, output: 20 },
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("SessionRunnerModel", () => {
|
describe("ModelResolver", () => {
|
||||||
it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () =>
|
it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
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(catalog.id).toBe(ModelV2.ID.make("test-model"))
|
||||||
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
|
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", () =>
|
it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { apiKey: "secret", baseURL: "https://openai.example/v1" },
|
settings: { apiKey: "secret", baseURL: "https://openai.example/v1" },
|
||||||
}),
|
}),
|
||||||
|
|
@ -82,7 +78,7 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("treats an empty configured API key as omitted", () =>
|
it.effect("treats an empty configured API key as omitted", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { apiKey: "", baseURL: "https://openai.example/v1" },
|
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", () =>
|
it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
|
||||||
compatibility: { reasoningField: "vendor_reasoning" },
|
compatibility: { reasoningField: "vendor_reasoning" },
|
||||||
settings: {
|
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* () {
|
Effect.gen(function* () {
|
||||||
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
|
|
@ -147,22 +143,7 @@ describe("SessionRunnerModel", () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
const session = SessionV2.Info.make({
|
const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high"))
|
||||||
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)
|
|
||||||
|
|
||||||
expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" })
|
expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" })
|
||||||
expect(resolved.route.defaults.http?.body).toEqual({
|
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* () {
|
Effect.gen(function* () {
|
||||||
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
|
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
|
||||||
settings: { baseURL: "https://compatible.example/v1" },
|
settings: { baseURL: "https://compatible.example/v1" },
|
||||||
|
|
@ -190,18 +171,7 @@ describe("SessionRunnerModel", () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
const session = SessionV2.Info.make({
|
const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high"))
|
||||||
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)
|
|
||||||
|
|
||||||
expect(resolved.route.defaults.http?.body).toEqual({
|
expect(resolved.route.defaults.http?.body).toEqual({
|
||||||
custom_extension: { enabled: true },
|
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* () {
|
Effect.gen(function* () {
|
||||||
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
})
|
})
|
||||||
const session = SessionV2.Info.make({
|
const failure = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("unknown")).pipe(Effect.flip)
|
||||||
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)
|
|
||||||
|
|
||||||
expect(failure).toMatchObject({
|
expect(failure).toMatchObject({
|
||||||
_tag: "SessionRunnerModel.VariantUnavailableError",
|
_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* () {
|
Effect.gen(function* () {
|
||||||
const catalog = model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
|
const catalog = model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
|
||||||
settings: { baseURL: "https://anthropic.example/v1" },
|
settings: { baseURL: "https://anthropic.example/v1" },
|
||||||
|
|
@ -256,18 +211,7 @@ describe("SessionRunnerModel", () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
const session = SessionV2.Info.make({
|
const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high"))
|
||||||
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)
|
|
||||||
|
|
||||||
expect(resolved.route.defaults.http?.body).toEqual({
|
expect(resolved.route.defaults.http?.body).toEqual({
|
||||||
custom_extension: { enabled: true },
|
custom_extension: { enabled: true },
|
||||||
|
|
@ -280,7 +224,7 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
|
it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
|
model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
|
||||||
settings: { baseURL: "https://anthropic.example/v1" },
|
settings: { baseURL: "https://anthropic.example/v1" },
|
||||||
}),
|
}),
|
||||||
|
|
@ -296,7 +240,7 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("uses resolved credentials for bearer auth", () =>
|
it.effect("uses resolved credentials for bearer auth", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
headers: {},
|
headers: {},
|
||||||
|
|
@ -320,7 +264,7 @@ describe("SessionRunnerModel", () => {
|
||||||
it.effect("prefers stored credentials over configured auth", () =>
|
it.effect("prefers stored credentials over configured auth", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
|
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"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" },
|
settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" },
|
||||||
headers: {},
|
headers: {},
|
||||||
|
|
@ -343,7 +287,7 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("does not project OAuth account metadata into the request body", () =>
|
it.effect("does not project OAuth account metadata into the request body", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
headers: {},
|
headers: {},
|
||||||
|
|
@ -365,7 +309,7 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("routes ChatGPT OAuth credentials to the codex backend", () =>
|
it.effect("routes ChatGPT OAuth credentials to the codex backend", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
headers: {},
|
headers: {},
|
||||||
|
|
@ -400,7 +344,7 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("routes native OpenAI provider packages with ChatGPT credentials to the codex backend", () =>
|
it.effect("routes native OpenAI provider packages with ChatGPT credentials to the codex backend", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model("@opencode-ai/ai/providers/openai", {
|
model("@opencode-ai/ai/providers/openai", {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
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", () =>
|
it.effect("does not route native OpenAI-compatible packages to the codex backend", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model("@opencode-ai/ai/providers/openai-compatible", {
|
model("@opencode-ai/ai/providers/openai-compatible", {
|
||||||
settings: { baseURL: "https://compatible.example/v1" },
|
settings: { baseURL: "https://compatible.example/v1" },
|
||||||
}),
|
}),
|
||||||
|
|
@ -450,7 +394,7 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("maps legacy OpenAI organization and project settings to headers", () =>
|
it.effect("maps legacy OpenAI organization and project settings to headers", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { organization: "org_123", project: "proj_123" },
|
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", () =>
|
it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
headers: {},
|
headers: {},
|
||||||
|
|
@ -496,7 +440,7 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () =>
|
it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
headers: {},
|
headers: {},
|
||||||
|
|
@ -528,12 +472,12 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("loads dynamic native provider packages through the injected package loader", () =>
|
it.effect("loads dynamic native provider packages through the injected package loader", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const native = yield* SessionRunnerModel.fromCatalogModel(
|
const native = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model("@opencode-ai/ai/providers/custom", {
|
model("@opencode-ai/ai/providers/custom", {
|
||||||
settings: { region: "test" },
|
settings: { region: "test" },
|
||||||
headers: { "x-package": "header" },
|
headers: { "x-package": "header" },
|
||||||
|
|
@ -565,7 +509,7 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("maps OAuth credentials to native provider auth settings", () =>
|
it.effect("maps OAuth credentials to native provider auth settings", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const native = yield* SessionRunnerModel.fromCatalogModel(
|
const native = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
}),
|
}),
|
||||||
|
|
@ -588,7 +532,7 @@ describe("SessionRunnerModel", () => {
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
yield* Effect.forEach(packages, ([specifier, key]) =>
|
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: () =>
|
loadPackage: () =>
|
||||||
Effect.succeed({
|
Effect.succeed({
|
||||||
model: (modelID, settings) => {
|
model: (modelID, settings) => {
|
||||||
|
|
@ -604,12 +548,12 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("loads arbitrary AISDK packages through the injected AISDK loader", () =>
|
it.effect("loads arbitrary AISDK packages through the injected AISDK loader", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const native = yield* SessionRunnerModel.fromCatalogModel(
|
const native = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/google"), {
|
model(ProviderV2.aisdk("@ai-sdk/google"), {
|
||||||
modelID: "gemini-api-model",
|
modelID: "gemini-api-model",
|
||||||
settings: { project: "test" },
|
settings: { project: "test" },
|
||||||
|
|
@ -644,7 +588,7 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("rejects AISDK packages without an available loader", () =>
|
it.effect("rejects AISDK packages without an available loader", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const failure = yield* SessionRunnerModel.fromCatalogModel(
|
const failure = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/google"), {
|
model(ProviderV2.aisdk("@ai-sdk/google"), {
|
||||||
settings: { baseURL: "https://google.example/v1" },
|
settings: { baseURL: "https://google.example/v1" },
|
||||||
}),
|
}),
|
||||||
|
|
@ -662,12 +606,12 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("drops an empty API key before loading an AISDK package", () =>
|
it.effect("drops an empty API key before loading an AISDK package", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const native = yield* SessionRunnerModel.fromCatalogModel(
|
const native = yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
settings: { baseURL: "https://openai.example/v1" },
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* SessionRunnerModel.fromCatalogModel(
|
yield* ModelResolver.fromCatalogModel(
|
||||||
model(ProviderV2.aisdk("@ai-sdk/google"), {
|
model(ProviderV2.aisdk("@ai-sdk/google"), {
|
||||||
settings: { apiKey: "", baseURL: "https://google.example/v1" },
|
settings: { apiKey: "", baseURL: "https://google.example/v1" },
|
||||||
}),
|
}),
|
||||||
|
|
@ -685,9 +629,9 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("reports whether a catalog model declares a provider package", () =>
|
it.effect("reports whether a catalog model declares a provider package", () =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
expect(SessionRunnerModel.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true)
|
expect(ModelResolver.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true)
|
||||||
expect(SessionRunnerModel.supported(model("@opencode-ai/ai/providers/custom"))).toBe(true)
|
expect(ModelResolver.supported(model("@opencode-ai/ai/providers/custom"))).toBe(true)
|
||||||
expect(SessionRunnerModel.supported(model(undefined))).toBe(false)
|
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")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
@ -108,7 +108,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -134,7 +134,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -169,7 +169,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -190,7 +190,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -210,7 +210,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -230,7 +230,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -251,7 +251,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -281,7 +281,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -310,7 +310,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
||||||
modelID: ModelV2.ID.make("openai.gpt-5.5"),
|
modelID: ModelV2.ID.make("openai.gpt-5.5"),
|
||||||
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
|
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -338,7 +338,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
||||||
modelID: ModelV2.ID.make("openai.gpt-5.5"),
|
modelID: ModelV2.ID.make("openai.gpt-5.5"),
|
||||||
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
|
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -347,7 +347,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
|
||||||
modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
|
modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
|
||||||
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
|
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -365,7 +365,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/anthropic"),
|
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/anthropic"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -393,7 +393,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -425,7 +425,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -434,7 +434,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -443,7 +443,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -452,7 +452,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -461,7 +461,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -487,7 +487,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -574,7 +574,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
for (const item of cases) {
|
for (const item of cases) {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
|
...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
|
||||||
modelID: ModelV2.ID.make(item.modelID),
|
modelID: ModelV2.ID.make(item.modelID),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -594,7 +594,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ describe("AnthropicPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
|
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -81,7 +81,7 @@ describe("AnthropicPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
|
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
||||||
modelID: ModelV2.ID.make("deployment"),
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -140,7 +140,7 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
||||||
modelID: ModelV2.ID.make("deployment"),
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -149,7 +149,7 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
})
|
})
|
||||||
const ignored = yield* aisdk.runLanguage({
|
const ignored = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
||||||
modelID: ModelV2.ID.make("deployment"),
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -170,7 +170,7 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")),
|
||||||
modelID: ModelV2.ID.make("messages-deployment"),
|
modelID: ModelV2.ID.make("messages-deployment"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -179,7 +179,7 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
|
||||||
modelID: ModelV2.ID.make("chat-deployment"),
|
modelID: ModelV2.ID.make("chat-deployment"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -188,7 +188,7 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")),
|
||||||
modelID: ModelV2.ID.make("language-deployment"),
|
modelID: ModelV2.ID.make("language-deployment"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ describe("AzurePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
modelID: ModelV2.ID.make("deployment"),
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -168,7 +168,7 @@ describe("AzurePlugin", () => {
|
||||||
const exit = yield* aisdk
|
const exit = yield* aisdk
|
||||||
.runSDK({
|
.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
modelID: ModelV2.ID.make("deployment"),
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -189,7 +189,7 @@ describe("AzurePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
modelID: ModelV2.ID.make("deployment"),
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -208,7 +208,7 @@ describe("AzurePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
modelID: ModelV2.ID.make("deployment"),
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -227,7 +227,7 @@ describe("AzurePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
modelID: ModelV2.ID.make("deployment"),
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
body: { useCompletionUrls: true },
|
body: { useCompletionUrls: true },
|
||||||
|
|
@ -247,7 +247,7 @@ describe("AzurePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
modelID: ModelV2.ID.make("deployment"),
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -256,7 +256,7 @@ describe("AzurePlugin", () => {
|
||||||
})
|
})
|
||||||
const ignored = yield* aisdk.runLanguage({
|
const ignored = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
||||||
modelID: ModelV2.ID.make("deployment"),
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -280,7 +280,7 @@ describe("AzurePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
|
||||||
modelID: ModelV2.ID.make("messages-deployment"),
|
modelID: ModelV2.ID.make("messages-deployment"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -289,7 +289,7 @@ describe("AzurePlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
|
...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
|
||||||
modelID: ModelV2.ID.make("language-deployment"),
|
modelID: ModelV2.ID.make("language-deployment"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ describe("CerebrasPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.default(
|
||||||
ProviderV2.ID.make("custom-cerebras"),
|
ProviderV2.ID.make("custom-cerebras"),
|
||||||
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||||
),
|
),
|
||||||
|
|
@ -88,7 +88,7 @@ describe("CerebrasPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.default(
|
||||||
ProviderV2.ID.make("custom-cerebras"),
|
ProviderV2.ID.make("custom-cerebras"),
|
||||||
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||||
),
|
),
|
||||||
|
|
@ -110,7 +110,7 @@ describe("CerebrasPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.default(
|
||||||
ProviderV2.ID.make("custom-cerebras"),
|
ProviderV2.ID.make("custom-cerebras"),
|
||||||
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -139,7 +139,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
|
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -184,7 +184,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
|
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -214,7 +214,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
|
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -252,7 +252,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
|
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -284,7 +284,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
|
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -307,7 +307,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -331,7 +331,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -361,7 +361,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -385,7 +385,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.default(
|
||||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||||
ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
|
ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
|
||||||
),
|
),
|
||||||
|
|
@ -417,7 +417,7 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))
|
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))
|
||||||
const sdk = yield* aisdk.runSDK({
|
const sdk = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||||
modelID: ModelV2.ID.make("@cf/model"),
|
modelID: ModelV2.ID.make("@cf/model"),
|
||||||
package: provider.package,
|
package: provider.package,
|
||||||
settings: provider.settings,
|
settings: provider.settings,
|
||||||
|
|
@ -138,7 +138,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||||
modelID: ModelV2.ID.make("@cf/model"),
|
modelID: ModelV2.ID.make("@cf/model"),
|
||||||
package: "aisdk:@ai-sdk/openai-compatible",
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
settings: { baseURL: "https://proxy.example/v1" },
|
settings: { baseURL: "https://proxy.example/v1" },
|
||||||
|
|
@ -178,7 +178,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||||
modelID: ModelV2.ID.make("@cf/model"),
|
modelID: ModelV2.ID.make("@cf/model"),
|
||||||
package: "aisdk:@ai-sdk/openai-compatible",
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
settings: { baseURL: "https://proxy.example/v1" },
|
settings: { baseURL: "https://proxy.example/v1" },
|
||||||
|
|
@ -207,7 +207,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||||
modelID: ModelV2.ID.make("@cf/model"),
|
modelID: ModelV2.ID.make("@cf/model"),
|
||||||
package: "aisdk:@ai-sdk/openai-compatible",
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" },
|
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" },
|
||||||
|
|
@ -233,7 +233,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
|
||||||
modelID: ModelV2.ID.make("@cf/api-model"),
|
modelID: ModelV2.ID.make("@cf/api-model"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -253,7 +253,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||||
modelID: ModelV2.ID.make("@cf/model"),
|
modelID: ModelV2.ID.make("@cf/model"),
|
||||||
package: "aisdk:@ai-sdk/anthropic",
|
package: "aisdk:@ai-sdk/anthropic",
|
||||||
settings: { baseURL: "https://proxy.example/v1" },
|
settings: { baseURL: "https://proxy.example/v1" },
|
||||||
|
|
|
||||||
|
|
@ -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"])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
@ -53,7 +53,7 @@ describe("DynamicProviderPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
|
||||||
modelID: ModelV2.ID.make("test-model"),
|
modelID: ModelV2.ID.make("test-model"),
|
||||||
package: ProviderV2.aisdk(fixtureProvider),
|
package: ProviderV2.aisdk(fixtureProvider),
|
||||||
}),
|
}),
|
||||||
|
|
@ -72,7 +72,7 @@ describe("DynamicProviderPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
|
||||||
modelID: ModelV2.ID.make("test-model"),
|
modelID: ModelV2.ID.make("test-model"),
|
||||||
package: ProviderV2.aisdk(fixtureProvider),
|
package: ProviderV2.aisdk(fixtureProvider),
|
||||||
}),
|
}),
|
||||||
|
|
@ -90,7 +90,7 @@ describe("DynamicProviderPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
|
||||||
modelID: ModelV2.ID.make("test-model"),
|
modelID: ModelV2.ID.make("test-model"),
|
||||||
package: ProviderV2.aisdk(fixtureProvider),
|
package: ProviderV2.aisdk(fixtureProvider),
|
||||||
}),
|
}),
|
||||||
|
|
@ -107,7 +107,7 @@ describe("DynamicProviderPlugin", () => {
|
||||||
yield* addPlugin(npmEntrypoint(fixtureProviderPath))
|
yield* addPlugin(npmEntrypoint(fixtureProviderPath))
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
|
||||||
modelID: ModelV2.ID.make("test-model"),
|
modelID: ModelV2.ID.make("test-model"),
|
||||||
package: "aisdk:fixture-provider",
|
package: "aisdk:fixture-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -125,7 +125,7 @@ describe("DynamicProviderPlugin", () => {
|
||||||
const exit = yield* aisdk
|
const exit = yield* aisdk
|
||||||
.language(
|
.language(
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.default(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")),
|
||||||
modelID: ModelV2.ID.make("alias"),
|
modelID: ModelV2.ID.make("alias"),
|
||||||
package: "aisdk:fixture-provider",
|
package: "aisdk:fixture-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -143,7 +143,7 @@ describe("DynamicProviderPlugin", () => {
|
||||||
const exit = yield* aisdk
|
const exit = yield* aisdk
|
||||||
.language(
|
.language(
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.default(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")),
|
||||||
modelID: ModelV2.ID.make("alias"),
|
modelID: ModelV2.ID.make("alias"),
|
||||||
package: "aisdk:file:///missing/provider-factory.js",
|
package: "aisdk:file:///missing/provider-factory.js",
|
||||||
}),
|
}),
|
||||||
|
|
@ -163,7 +163,7 @@ describe("DynamicProviderPlugin", () => {
|
||||||
const exit = yield* aisdk
|
const exit = yield* aisdk
|
||||||
.language(
|
.language(
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.default(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")),
|
||||||
modelID: ModelV2.ID.make("alias"),
|
modelID: ModelV2.ID.make("alias"),
|
||||||
package: "aisdk:fixture-provider",
|
package: "aisdk:fixture-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -181,7 +181,7 @@ describe("DynamicProviderPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const language = yield* aisdk.language(
|
const language = yield* aisdk.language(
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")),
|
||||||
modelID: ModelV2.ID.make("test-model-api"),
|
modelID: ModelV2.ID.make("test-model-api"),
|
||||||
package: ProviderV2.aisdk(fixtureProvider),
|
package: ProviderV2.aisdk(fixtureProvider),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
60
packages/core/test/plugin/provider-factory.test.ts
Normal file
60
packages/core/test/plugin/provider-factory.test.ts
Normal file
|
|
@ -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.default(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)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
@ -99,7 +99,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const ignored = yield* aisdk.runSDK({
|
const ignored = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -108,7 +108,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
})
|
})
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -128,7 +128,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4"),
|
modelID: ModelV2.ID.make("claude-sonnet-4"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -147,7 +147,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4"),
|
modelID: ModelV2.ID.make("claude-sonnet-4"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -166,7 +166,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -175,7 +175,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
|
||||||
modelID: ModelV2.ID.make("gpt-5.1-codex"),
|
modelID: ModelV2.ID.make("gpt-5.1-codex"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -184,7 +184,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
|
||||||
modelID: ModelV2.ID.make("gpt-4o"),
|
modelID: ModelV2.ID.make("gpt-4o"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -193,7 +193,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
|
||||||
modelID: ModelV2.ID.make("gpt-5-mini"),
|
modelID: ModelV2.ID.make("gpt-5-mini"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -202,7 +202,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")),
|
||||||
modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"),
|
modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -227,7 +227,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("mai-code-1-flash-picker")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("mai-code-1-flash-picker")),
|
||||||
modelID: ModelV2.ID.make("mai-code-1-flash-picker"),
|
modelID: ModelV2.ID.make("mai-code-1-flash-picker"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
settings: { endpoint: "responses" },
|
settings: { endpoint: "responses" },
|
||||||
|
|
@ -237,7 +237,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
settings: { endpoint: "chat" },
|
settings: { endpoint: "chat" },
|
||||||
|
|
@ -257,7 +257,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -266,7 +266,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
|
||||||
modelID: ModelV2.ID.make("gpt-5-mini"),
|
modelID: ModelV2.ID.make("gpt-5-mini"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -275,7 +275,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
|
...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4"),
|
modelID: ModelV2.ID.make("claude-sonnet-4"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -324,7 +324,7 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ describe("GitLabPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||||
modelID: ModelV2.ID.make("claude"),
|
modelID: ModelV2.ID.make("claude"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -107,7 +107,7 @@ describe("GitLabPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||||
modelID: ModelV2.ID.make("claude"),
|
modelID: ModelV2.ID.make("claude"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -133,7 +133,7 @@ describe("GitLabPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||||
modelID: ModelV2.ID.make("claude"),
|
modelID: ModelV2.ID.make("claude"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -175,7 +175,7 @@ describe("GitLabPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||||
modelID: ModelV2.ID.make("claude"),
|
modelID: ModelV2.ID.make("claude"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -195,7 +195,7 @@ describe("GitLabPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
|
...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
|
||||||
modelID: ModelV2.ID.make("duo-workflow-custom"),
|
modelID: ModelV2.ID.make("duo-workflow-custom"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
headers: {},
|
headers: {},
|
||||||
|
|
@ -229,7 +229,7 @@ describe("GitLabPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
|
...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
|
||||||
modelID: ModelV2.ID.make("duo-workflow-exact"),
|
modelID: ModelV2.ID.make("duo-workflow-exact"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -257,7 +257,7 @@ describe("GitLabPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
|
...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
|
||||||
modelID: ModelV2.ID.make("duo-workflow-custom"),
|
modelID: ModelV2.ID.make("duo-workflow-custom"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
headers: {},
|
headers: {},
|
||||||
|
|
@ -284,7 +284,7 @@ describe("GitLabPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||||
modelID: ModelV2.ID.make("claude"),
|
modelID: ModelV2.ID.make("claude"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
headers: { h: "v" },
|
headers: { h: "v" },
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.default(
|
||||||
ProviderV2.ID.make("google-vertex-anthropic"),
|
ProviderV2.ID.make("google-vertex-anthropic"),
|
||||||
ModelV2.ID.make("claude-sonnet-4-5"),
|
ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
),
|
),
|
||||||
|
|
@ -143,7 +143,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(
|
...ModelV2.Info.default(
|
||||||
ProviderV2.ID.make("google-vertex-anthropic"),
|
ProviderV2.ID.make("google-vertex-anthropic"),
|
||||||
ModelV2.ID.make("claude-sonnet-4-5"),
|
ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
),
|
),
|
||||||
|
|
@ -167,7 +167,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -187,7 +187,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -206,7 +206,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||||
const sdkResult = yield* aisdk.runSDK({
|
const sdkResult = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
||||||
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
|
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -215,7 +215,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
})
|
})
|
||||||
const languageResult = yield* aisdk.runLanguage({
|
const languageResult = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
||||||
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
|
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -238,7 +238,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
||||||
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
|
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -257,7 +257,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ describe("GoogleVertexPlugin", () => {
|
||||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
||||||
modelID: ModelV2.ID.make("gemini"),
|
modelID: ModelV2.ID.make("gemini"),
|
||||||
package: "aisdk:@ai-sdk/google-vertex",
|
package: "aisdk:@ai-sdk/google-vertex",
|
||||||
}),
|
}),
|
||||||
|
|
@ -294,7 +294,7 @@ describe("GoogleVertexPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
||||||
modelID: ModelV2.ID.make("gemini"),
|
modelID: ModelV2.ID.make("gemini"),
|
||||||
package: "aisdk:@ai-sdk/google-vertex",
|
package: "aisdk:@ai-sdk/google-vertex",
|
||||||
}),
|
}),
|
||||||
|
|
@ -339,7 +339,7 @@ describe("GoogleVertexPlugin", () => {
|
||||||
() =>
|
() =>
|
||||||
aisdk.runSDK({
|
aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
||||||
modelID: ModelV2.ID.make("gemini"),
|
modelID: ModelV2.ID.make("gemini"),
|
||||||
package: "aisdk:@ai-sdk/openai-compatible",
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
}),
|
}),
|
||||||
|
|
@ -367,7 +367,7 @@ describe("GoogleVertexPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
|
||||||
modelID: ModelV2.ID.make(" gemini-2.5-pro "),
|
modelID: ModelV2.ID.make(" gemini-2.5-pro "),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ describe("GooglePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
|
||||||
modelID: ModelV2.ID.make("gemini"),
|
modelID: ModelV2.ID.make("gemini"),
|
||||||
package: "aisdk:@ai-sdk/google",
|
package: "aisdk:@ai-sdk/google",
|
||||||
}),
|
}),
|
||||||
|
|
@ -45,7 +45,7 @@ describe("GooglePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
|
...ModelV2.Info.default(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
|
||||||
modelID: ModelV2.ID.make("gemini"),
|
modelID: ModelV2.ID.make("gemini"),
|
||||||
package: "aisdk:@ai-sdk/google",
|
package: "aisdk:@ai-sdk/google",
|
||||||
}),
|
}),
|
||||||
|
|
@ -63,7 +63,7 @@ describe("GooglePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const sdkEvent = yield* aisdk.runSDK({
|
const sdkEvent = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
|
||||||
modelID: ModelV2.ID.make("gemini-api"),
|
modelID: ModelV2.ID.make("gemini-api"),
|
||||||
package: "aisdk:@ai-sdk/google",
|
package: "aisdk:@ai-sdk/google",
|
||||||
}),
|
}),
|
||||||
|
|
@ -88,7 +88,7 @@ describe("GooglePlugin", () => {
|
||||||
|
|
||||||
const resolved = yield* aisdk.model(
|
const resolved = yield* aisdk.model(
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
|
||||||
modelID: ModelV2.ID.make("gemini-api"),
|
modelID: ModelV2.ID.make("gemini-api"),
|
||||||
package: "aisdk:@ai-sdk/google",
|
package: "aisdk:@ai-sdk/google",
|
||||||
settings: { apiKey: "test" },
|
settings: { apiKey: "test" },
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
@ -26,7 +26,7 @@ describe("OpenAICompatiblePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const defaulted = yield* aisdk.runSDK({
|
const defaulted = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
||||||
modelID: ModelV2.ID.make("model"),
|
modelID: ModelV2.ID.make("model"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -35,7 +35,7 @@ describe("OpenAICompatiblePlugin", () => {
|
||||||
})
|
})
|
||||||
const disabled = yield* aisdk.runSDK({
|
const disabled = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
||||||
modelID: ModelV2.ID.make("model"),
|
modelID: ModelV2.ID.make("model"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -54,7 +54,7 @@ describe("OpenAICompatiblePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
||||||
modelID: ModelV2.ID.make("model"),
|
modelID: ModelV2.ID.make("model"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -78,7 +78,7 @@ describe("OpenAICompatiblePlugin", () => {
|
||||||
)
|
)
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")),
|
||||||
modelID: ModelV2.ID.make("model"),
|
modelID: ModelV2.ID.make("model"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -99,7 +99,7 @@ describe("OpenAICompatiblePlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")),
|
...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")),
|
||||||
modelID: ModelV2.ID.make("model"),
|
modelID: ModelV2.ID.make("model"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ describe("OpenAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -86,7 +86,7 @@ describe("OpenAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -105,7 +105,7 @@ describe("OpenAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
|
...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -125,7 +125,7 @@ describe("OpenAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
|
||||||
modelID: ModelV2.ID.make("gpt-5"),
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ describe("OpencodePlugin", () => {
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")),
|
||||||
modelID: ModelV2.ID.make("paid"),
|
modelID: ModelV2.ID.make("paid"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(1),
|
cost: cost(1),
|
||||||
|
|
@ -320,7 +320,7 @@ describe("OpencodePlugin", () => {
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")),
|
...ModelV2.Info.default(provider.id, ModelV2.ID.make("free")),
|
||||||
modelID: ModelV2.ID.make("free"),
|
modelID: ModelV2.ID.make("free"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(0),
|
cost: cost(0),
|
||||||
|
|
@ -347,7 +347,7 @@ describe("OpencodePlugin", () => {
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")),
|
...ModelV2.Info.default(provider.id, ModelV2.ID.make("output-only")),
|
||||||
modelID: ModelV2.ID.make("output-only"),
|
modelID: ModelV2.ID.make("output-only"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(0, 1),
|
cost: cost(0, 1),
|
||||||
|
|
@ -376,7 +376,7 @@ describe("OpencodePlugin", () => {
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")),
|
||||||
modelID: ModelV2.ID.make("paid"),
|
modelID: ModelV2.ID.make("paid"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(1),
|
cost: cost(1),
|
||||||
|
|
@ -410,7 +410,7 @@ describe("OpencodePlugin", () => {
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")),
|
||||||
modelID: ModelV2.ID.make("paid"),
|
modelID: ModelV2.ID.make("paid"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(1),
|
cost: cost(1),
|
||||||
|
|
@ -438,7 +438,7 @@ describe("OpencodePlugin", () => {
|
||||||
settings: { apiKey: "configured" },
|
settings: { apiKey: "configured" },
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")),
|
||||||
modelID: ModelV2.ID.make("paid"),
|
modelID: ModelV2.ID.make("paid"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(1),
|
cost: cost(1),
|
||||||
|
|
@ -468,7 +468,7 @@ describe("OpencodePlugin", () => {
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")),
|
||||||
modelID: ModelV2.ID.make("paid"),
|
modelID: ModelV2.ID.make("paid"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(1),
|
cost: cost(1),
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ describe("OpenRouterPlugin", () => {
|
||||||
|
|
||||||
const ignored = yield* aisdk.runSDK({
|
const ignored = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
@ -65,7 +65,7 @@ describe("OpenRouterPlugin", () => {
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
modelID: ModelV2.ID.make("openai/gpt-5"),
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
package: ProviderV2.aisdk("test-provider"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
@ -48,7 +48,7 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
|
||||||
|
|
||||||
function model(providerID: string) {
|
function model(providerID: string) {
|
||||||
return ModelV2.Info.make({
|
return ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")),
|
...ModelV2.Info.default(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")),
|
||||||
modelID: ModelV2.ID.make("sap-model"),
|
modelID: ModelV2.ID.make("sap-model"),
|
||||||
package: ProviderV2.aisdk(fixtureProvider),
|
package: ProviderV2.aisdk(fixtureProvider),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")),
|
...ModelV2.Info.default(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")),
|
||||||
modelID: ModelV2.ID.make("gpt-4"),
|
modelID: ModelV2.ID.make("gpt-4"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -77,7 +77,7 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -97,7 +97,7 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -121,7 +121,7 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -141,7 +141,7 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
@ -165,7 +165,7 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
||||||
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
||||||
package: "aisdk:test-provider",
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
@ -59,7 +59,7 @@ describe("VercelPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const event = yield* aisdk.runSDK({
|
const event = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")),
|
||||||
modelID: ModelV2.ID.make("v0-1.0-md"),
|
modelID: ModelV2.ID.make("v0-1.0-md"),
|
||||||
package: "aisdk:@ai-sdk/vercel",
|
package: "aisdk:@ai-sdk/vercel",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ describe("XAIPlugin", () => {
|
||||||
|
|
||||||
const ignored = yield* aisdk.runSDK({
|
const ignored = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
||||||
modelID: ModelV2.ID.make("grok-4"),
|
modelID: ModelV2.ID.make("grok-4"),
|
||||||
package: "aisdk:@ai-sdk/xai",
|
package: "aisdk:@ai-sdk/xai",
|
||||||
}),
|
}),
|
||||||
|
|
@ -72,7 +72,7 @@ describe("XAIPlugin", () => {
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
||||||
modelID: ModelV2.ID.make("grok-4"),
|
modelID: ModelV2.ID.make("grok-4"),
|
||||||
package: "aisdk:@ai-sdk/xai",
|
package: "aisdk:@ai-sdk/xai",
|
||||||
}),
|
}),
|
||||||
|
|
@ -92,7 +92,7 @@ describe("XAIPlugin", () => {
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")),
|
...ModelV2.Info.default(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")),
|
||||||
modelID: ModelV2.ID.make("grok-4"),
|
modelID: ModelV2.ID.make("grok-4"),
|
||||||
package: "aisdk:@ai-sdk/xai",
|
package: "aisdk:@ai-sdk/xai",
|
||||||
}),
|
}),
|
||||||
|
|
@ -112,7 +112,7 @@ describe("XAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")),
|
||||||
modelID: ModelV2.ID.make("grok-4"),
|
modelID: ModelV2.ID.make("grok-4"),
|
||||||
package: "aisdk:@ai-sdk/xai",
|
package: "aisdk:@ai-sdk/xai",
|
||||||
}),
|
}),
|
||||||
|
|
@ -133,7 +133,7 @@ describe("XAIPlugin", () => {
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")),
|
...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")),
|
||||||
modelID: ModelV2.ID.make("grok-4"),
|
modelID: ModelV2.ID.make("grok-4"),
|
||||||
package: "aisdk:@ai-sdk/xai",
|
package: "aisdk:@ai-sdk/xai",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,15 @@ const client = Layer.mock(LLMClient.Service)({
|
||||||
generate: () => Effect.die("unused"),
|
generate: () => Effect.die("unused"),
|
||||||
})
|
})
|
||||||
const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
|
const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
|
||||||
const models = SessionRunnerModel.layerWith(() =>
|
const models = Layer.mock(SessionRunnerModel.Service)({
|
||||||
Effect.succeed(
|
resolve: () =>
|
||||||
SessionRunnerModel.resolved(model, {
|
Effect.succeed(
|
||||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
SessionRunnerModel.resolved(model, {
|
||||||
cost: [],
|
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||||
}),
|
cost: [],
|
||||||
),
|
}),
|
||||||
)
|
),
|
||||||
|
})
|
||||||
const locations = Layer.effect(
|
const locations = Layer.effect(
|
||||||
LocationServiceMap.Service,
|
LocationServiceMap.Service,
|
||||||
LayerMap.make(
|
LayerMap.make(
|
||||||
|
|
|
||||||
|
|
@ -66,14 +66,15 @@ const client = Layer.mock(LLMClient.Service)({
|
||||||
return response
|
return response
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const models = SessionRunnerModel.layerWith(() =>
|
const models = Layer.mock(SessionRunnerModel.Service)({
|
||||||
Effect.succeed(
|
resolve: () =>
|
||||||
SessionRunnerModel.resolved(model, {
|
Effect.succeed(
|
||||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
SessionRunnerModel.resolved(model, {
|
||||||
cost: [],
|
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||||
}),
|
cost: [],
|
||||||
),
|
}),
|
||||||
)
|
),
|
||||||
|
})
|
||||||
const builtins = Layer.mock(InstructionBuiltIns.Service, {
|
const builtins = Layer.mock(InstructionBuiltIns.Service, {
|
||||||
load: () =>
|
load: () =>
|
||||||
Effect.succeed(
|
Effect.succeed(
|
||||||
|
|
@ -300,7 +301,7 @@ it.effect("generates from fresh settled Session context without durable mutation
|
||||||
),
|
),
|
||||||
).toEqual(["Settled partial answer"])
|
).toEqual(["Settled partial answer"])
|
||||||
expect(requests[0]?.tools).toMatchObject([{ name: "lookup", description: "Hooked lookup" }])
|
expect(requests[0]?.tools).toMatchObject([{ name: "lookup", description: "Hooked lookup" }])
|
||||||
expect(requests[0]?.toolChoice).toMatchObject({ type: "none" })
|
expect(requests[0]?.toolChoice).toBeUndefined()
|
||||||
expect(yield* durableState(db, sessionID)).toEqual(before)
|
expect(yield* durableState(db, sessionID)).toEqual(before)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -73,14 +73,15 @@ const model = OpenAIChat.route
|
||||||
generation: { maxTokens: 20, temperature: 0 },
|
generation: { maxTokens: 20, temperature: 0 },
|
||||||
})
|
})
|
||||||
.model({ id: "gpt-4o-mini" })
|
.model({ id: "gpt-4o-mini" })
|
||||||
const models = SessionRunnerModel.layerWith(() =>
|
const models = Layer.mock(SessionRunnerModel.Service)({
|
||||||
Effect.succeed(
|
resolve: () =>
|
||||||
SessionRunnerModel.resolved(model, {
|
Effect.succeed(
|
||||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
SessionRunnerModel.resolved(model, {
|
||||||
cost: [],
|
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||||
}),
|
cost: [],
|
||||||
),
|
}),
|
||||||
)
|
),
|
||||||
|
})
|
||||||
const systemContext = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed(Instructions.empty) })
|
const systemContext = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||||
const instructionContext = Layer.mock(InstructionDiscovery.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) })
|
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] })
|
const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] })
|
||||||
let modelResolveHook = Effect.void
|
let modelResolveHook = Effect.void
|
||||||
let currentModel = model
|
let currentModel = model
|
||||||
const models = SessionRunnerModel.layerWith((session) =>
|
const models = Layer.mock(SessionRunnerModel.Service)({
|
||||||
modelResolveHook.pipe(
|
resolve: (session) =>
|
||||||
Effect.as(
|
modelResolveHook.pipe(
|
||||||
SessionRunnerModel.resolved(session.model?.id === "replacement" ? replacementModel : currentModel, {
|
Effect.as(
|
||||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
SessionRunnerModel.resolved(session.model?.id === "replacement" ? replacementModel : currentModel, {
|
||||||
cost: [],
|
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||||
variant: session.model?.variant,
|
cost: [],
|
||||||
}),
|
variant: session.model?.variant,
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
})
|
||||||
)
|
|
||||||
const systemContextKey = Instructions.Key.make("test/context")
|
const systemContextKey = Instructions.Key.make("test/context")
|
||||||
let systemBaseline = "Initial context"
|
let systemBaseline = "Initial context"
|
||||||
let systemRemoved = false
|
let systemRemoved = false
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,8 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||||
for (const [core, shared] of schemas) expect(core).toBe(shared)
|
for (const [core, shared] of schemas) expect(core).toBe(shared)
|
||||||
|
|
||||||
expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(AgentV2.Info.empty(AgentV2.ID.make("test")))
|
expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(AgentV2.Info.empty(AgentV2.ID.make("test")))
|
||||||
expect(Model.Info.empty(Provider.ID.make("test"), Model.ID.make("model"))).toEqual(
|
expect(Model.Info.default(Provider.ID.make("test"), Model.ID.make("model"))).toEqual(
|
||||||
ModelV2.Info.empty(ProviderV2.ID.make("test"), ModelV2.ID.make("model")),
|
ModelV2.Info.default(ProviderV2.ID.make("test"), ModelV2.ID.make("model")),
|
||||||
)
|
)
|
||||||
expect(Provider.Info.empty(Provider.ID.make("test"))).toEqual(ProviderV2.Info.empty(ProviderV2.ID.make("test")))
|
expect(Provider.Info.empty(Provider.ID.make("test"))).toEqual(ProviderV2.Info.empty(ProviderV2.ID.make("test")))
|
||||||
expect(Skill.Source.key(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/tmp") }))).toBe(
|
expect(Skill.Source.key(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/tmp") }))).toBe(
|
||||||
|
|
|
||||||
|
|
@ -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", () =>
|
it.live("rejects deleting a directory", () =>
|
||||||
withTempTool((directory, registry) =>
|
withTempTool((directory, registry) =>
|
||||||
Effect.gen(function* () {
|
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", () =>
|
it.live("rejects a missing second chunk context", () =>
|
||||||
withTempTool((directory, registry) =>
|
withTempTool((directory, registry) =>
|
||||||
Effect.gen(function* () {
|
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", () =>
|
it.live("applies successive update operations to one file", () =>
|
||||||
withTempTool((directory, registry) =>
|
withTempTool((directory, registry) =>
|
||||||
Effect.gen(function* () {
|
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", () =>
|
it.live("rejects an update with missing context", () =>
|
||||||
withTempTool((directory, registry) =>
|
withTempTool((directory, registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,8 @@ describe("search tools", () => {
|
||||||
|
|
||||||
expect(glob.output?.structured).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT })
|
expect(glob.output?.structured).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT })
|
||||||
expect(grep.output?.structured).toEqual({ matches: 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(glob.output?.content).toEqual([{ type: "text", text: String(glob.result.value) }])
|
||||||
expect(grep.output?.content).toEqual([{ type: "text", text: grep.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(String(glob.result.value).split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT)
|
||||||
expect(grep.result.value).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`)
|
expect(grep.result.value).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,15 @@ bun validate
|
||||||
bun broken-links
|
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).
|
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
|
description: Reviews changes without modifying files
|
||||||
mode: subagent
|
mode: subagent
|
||||||
model: anthropic/claude-sonnet-4-5#high
|
model: anthropic/claude-sonnet-4-5#high
|
||||||
color: warning
|
color: "#ff6b6b"
|
||||||
steps: 8
|
steps: 8
|
||||||
permissions:
|
permissions:
|
||||||
- action: edit
|
- action: edit
|
||||||
|
|
@ -118,7 +118,7 @@ Use the `agents` field in any [OpenCode configuration file](/config):
|
||||||
"mode": "all",
|
"mode": "all",
|
||||||
"model": "anthropic/claude-sonnet-4-5#high",
|
"model": "anthropic/claude-sonnet-4-5#high",
|
||||||
"system": "Review the current changes. Report findings before any summary.",
|
"system": "Review the current changes. Report findings before any summary.",
|
||||||
"color": "warning",
|
"color": "#ff6b6b",
|
||||||
"steps": 8,
|
"steps": 8,
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{ "action": "edit", "resource": "*", "effect": "deny" },
|
{ "action": "edit", "resource": "*", "effect": "deny" },
|
||||||
|
|
@ -250,8 +250,7 @@ security boundary.
|
||||||
|
|
||||||
### `color`
|
### `color`
|
||||||
|
|
||||||
Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`, or one
|
Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`.
|
||||||
of `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info`.
|
|
||||||
|
|
||||||
### `disabled`
|
### `disabled`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"attachments",
|
"attachments",
|
||||||
"compaction",
|
"compaction",
|
||||||
"warming",
|
"warming",
|
||||||
|
"themes",
|
||||||
"formatters",
|
"formatters",
|
||||||
"lsp",
|
"lsp",
|
||||||
"references"
|
"references"
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,6 @@ limitations and safety details.
|
||||||
|
|
||||||
## Customize
|
## 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
|
keybinds](https://opencode.ai/docs/keybinds), [configuring formatters](/formatters), [creating commands](/commands), or
|
||||||
editing the [OpenCode config](/config).
|
editing the [OpenCode config](/config).
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,10 @@ You can also map a friendly catalog ID to a different API model ID with `modelID
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2` is sent to the provider. When adding a
|
Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2` is sent to the provider. A model that is
|
||||||
model that is not already in the catalog, set accurate `capabilities` and `limit` values so OpenCode can expose tools and
|
not already in the catalog defaults to tool support, text and image input, and text output. Set accurate `capabilities`
|
||||||
enforce the correct context limits. Set `disabled: true` on a model entry to hide it from the available catalog.
|
and `limit` values when those defaults do not match the model or OpenCode needs to enforce its context limits. Set
|
||||||
|
`disabled: true` on a model entry to hide it from the available catalog.
|
||||||
|
|
||||||
OpenAI-compatible models that stream reasoning through a custom assistant-message field can set
|
OpenAI-compatible models that stream reasoning through a custom assistant-message field can set
|
||||||
`compatibility.reasoningField`:
|
`compatibility.reasoningField`:
|
||||||
|
|
@ -193,8 +194,9 @@ For an OpenAI-compatible server, define a provider package, endpoint, and at lea
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the server's real model name, limits, modalities, and tool support. OpenCode cannot infer these for a model you add
|
Use the server's real model name, limits, modalities, and tool support. OpenCode applies the custom-model capability
|
||||||
manually. If the endpoint requires a key, add `apiKey` to provider `settings` using an environment substitution such as
|
defaults described above but cannot infer the server's actual limits or whether those defaults are accurate. If the
|
||||||
|
endpoint requires a key, add `apiKey` to provider `settings` using an environment substitution such as
|
||||||
`"apiKey": "{env:LOCAL_API_KEY}"`; do not commit secrets.
|
`"apiKey": "{env:LOCAL_API_KEY}"`; do not commit secrets.
|
||||||
|
|
||||||
### Model references
|
### Model references
|
||||||
|
|
|
||||||
|
|
@ -10561,26 +10561,10 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"Agent.Color": {
|
"Agent.Color": {
|
||||||
"anyOf": [
|
"type": "string",
|
||||||
|
"allOf": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"pattern": "^#[0-9a-fA-F]{6}$"
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"pattern": "^#[0-9a-fA-F]{6}$"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"primary",
|
|
||||||
"secondary",
|
|
||||||
"accent",
|
|
||||||
"success",
|
|
||||||
"warning",
|
|
||||||
"error",
|
|
||||||
"info"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@
|
||||||
"name": "@opencode-ai/docs",
|
"name": "@opencode-ai/docs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --bun mint dev --no-open --port 3333",
|
"dev": "bun run generate && bun --bun mint dev --no-open --port 3333",
|
||||||
"validate": "bun --bun mint validate",
|
"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"
|
"broken-links": "bun --bun mint broken-links"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mint": "4.2.666"
|
"effect": "catalog:",
|
||||||
|
"mint": "4.2.666",
|
||||||
|
"prettier": "3.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
136
packages/docs/script/generate-theme-tokens.ts
Normal file
136
packages/docs/script/generate-theme-tokens.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { Schema, SchemaAST } from "effect"
|
||||||
|
import { format } from "prettier"
|
||||||
|
import { ThemeDefinition, ThemeDocument } 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 ThemeDocument
|
||||||
|
Schema.decodeUnknownSync(ThemeDocument)(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)}`))
|
||||||
|
}
|
||||||
79
packages/docs/snippets/generated/theme-tokens.mdx
Normal file
79
packages/docs/snippets/generated/theme-tokens.mdx
Normal file
|
|
@ -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.
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue