chore: merge dev into v2 (#34788)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
This commit is contained in:
co-authored by
Brendan Allan
Kit Langton
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Affan Ali
affanali2k3
Frank
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Aiden Cline
Jay V
Dax Raad
Aarav Sareen
OpeOginni
Luke Parker
Ben Guthrie
Dax
Filip
Max Anderson
Brendan Allan
Jack
Shoubhit Dash
Dustin Deus
starptech
Aiden Cline
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
parent
932a40cfd9
commit
8c94e9005f
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Effect } from "effect"
|
||||
import path from "path"
|
||||
import { unlink } from "fs/promises"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
@@ -12,7 +13,7 @@ import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer))
|
||||
const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node])))
|
||||
|
||||
const originalEnv = new Map<string, string | undefined>()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from "bun:test"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
|
||||
import { Effect } from "effect"
|
||||
@@ -6,7 +7,7 @@ import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const DIGITALOCEAN = ProviderV2.ID.make("digitalocean")
|
||||
const it = testEffect(Provider.defaultLayer)
|
||||
const it = testEffect(LayerNode.compile(Provider.node))
|
||||
|
||||
const withEnv = <A, E, R>(values: Record<string, string>, effect: Effect.Effect<A, E, R>) =>
|
||||
Effect.acquireUseRelease(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { afterEach, expect } from "bun:test"
|
||||
import { createServer, type Server } from "node:http"
|
||||
import { streamText } from "ai"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Effect } from "effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
|
||||
@@ -18,7 +19,7 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer, CrossSpawnSpawner.defaultLayer),
|
||||
LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node, CrossSpawnSpawner.node])),
|
||||
)
|
||||
|
||||
it.live("headerTimeout does not abort delayed SSE body after headers arrive", () =>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { afterEach, expect, test } from "bun:test"
|
||||
import { mkdir, unlink } from "fs/promises"
|
||||
import path from "path"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
@@ -16,7 +18,8 @@ import { Provider } from "@/provider/provider"
|
||||
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { InstanceLayer } from "@/project/instance-layer"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
@@ -57,14 +60,18 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||
Provider.layer.pipe(
|
||||
Layer.provide(FSUtil.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Plugin.defaultLayer),
|
||||
Layer.provide(ModelsDev.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer(flags)),
|
||||
LayerNode.compile(
|
||||
LayerNode.group([
|
||||
Provider.node,
|
||||
FSUtil.node,
|
||||
Env.node,
|
||||
Config.node,
|
||||
Auth.node,
|
||||
Plugin.node,
|
||||
ModelsDev.node,
|
||||
RuntimeFlags.node,
|
||||
]),
|
||||
[[RuntimeFlags.node, RuntimeFlags.layer(flags)]],
|
||||
)
|
||||
|
||||
const list = Provider.use.list()
|
||||
@@ -77,7 +84,7 @@ const paid = (providers: Record<string, { models: Record<string, { cost: { input
|
||||
|
||||
const languageBaseURL = (language: unknown) => (language as { config: { baseURL: string } }).config.baseURL
|
||||
|
||||
const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer))
|
||||
const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node])))
|
||||
const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true }))
|
||||
|
||||
const alphaProviderConfig = {
|
||||
@@ -1755,8 +1762,11 @@ it.instance(
|
||||
// Tests that need plugin file setup or multi-instance flows fall back to a
|
||||
// scoped tmpdir + provideInstance pattern via it.effect.
|
||||
|
||||
const instanceStoreLayer = LayerNode.compile(InstanceStore.node, [
|
||||
[InstanceStore.bootstrapNode, InstanceBootstrap.node],
|
||||
])
|
||||
const provideMultiInstance = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
|
||||
eff.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer))
|
||||
eff.pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
|
||||
|
||||
it.effect("plugin config providers persist after instance dispose", () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -1859,7 +1869,7 @@ it.effect("opencode loader keeps paid models when config apiKey is present", ()
|
||||
Provider.use
|
||||
.list()
|
||||
.pipe(provideInstanceEffect(directory))
|
||||
.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer))
|
||||
.pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
|
||||
|
||||
const none = paid(yield* listIn(noneDir))
|
||||
const keyedCount = paid(yield* listIn(keyedDir))
|
||||
@@ -1878,7 +1888,7 @@ it.effect("opencode loader keeps paid models when auth exists", () =>
|
||||
Provider.use
|
||||
.list()
|
||||
.pipe(provideInstanceEffect(directory))
|
||||
.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer))
|
||||
.pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
|
||||
|
||||
const none = paid(yield* listIn(noneDir))
|
||||
|
||||
|
||||
@@ -613,6 +613,84 @@ describe("ProviderTransform.providerOptions", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("forces reasoning for custom OpenAI package models with explicit effort", () => {
|
||||
const model = createModel({
|
||||
providerID: "meta",
|
||||
api: {
|
||||
id: "muse-spark",
|
||||
url: "https://api.ai.meta.com/v1",
|
||||
npm: "@ai-sdk/openai",
|
||||
},
|
||||
})
|
||||
|
||||
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh", reasoningSummary: "auto" })).toEqual({
|
||||
openai: { forceReasoning: true, reasoningEffort: "xhigh", reasoningSummary: "auto" },
|
||||
})
|
||||
})
|
||||
|
||||
test("forces reasoning for OpenAI package models marked reasoning-capable", () => {
|
||||
expect(ProviderTransform.providerOptions(createModel(), { store: false })).toEqual({
|
||||
openai: { forceReasoning: true, store: false },
|
||||
})
|
||||
})
|
||||
|
||||
test("forces reasoning for explicit effort even when model is not marked reasoning-capable", () => {
|
||||
const model = createModel({
|
||||
capabilities: {
|
||||
temperature: true,
|
||||
reasoning: false,
|
||||
attachment: true,
|
||||
toolcall: true,
|
||||
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
||||
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||
interleaved: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh" })).toEqual({
|
||||
openai: { forceReasoning: true, reasoningEffort: "xhigh" },
|
||||
})
|
||||
})
|
||||
|
||||
test("forces reasoning for Azure OpenAI models with explicit effort", () => {
|
||||
const model = createModel({
|
||||
providerID: "azure",
|
||||
api: {
|
||||
id: "custom-gpt-5-deployment",
|
||||
url: "https://azure.openai.example.com/openai/v1",
|
||||
npm: "@ai-sdk/azure",
|
||||
},
|
||||
})
|
||||
|
||||
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh" })).toEqual({
|
||||
openai: { forceReasoning: true, reasoningEffort: "xhigh" },
|
||||
azure: { forceReasoning: true, reasoningEffort: "xhigh" },
|
||||
})
|
||||
})
|
||||
|
||||
test("forces reasoning for Bedrock Mantle OpenAI models with explicit effort", () => {
|
||||
const model = createModel({
|
||||
providerID: "amazon-bedrock",
|
||||
api: {
|
||||
id: "openai.gpt-5-custom",
|
||||
url: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
|
||||
npm: "@ai-sdk/amazon-bedrock/mantle",
|
||||
},
|
||||
})
|
||||
|
||||
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh" })).toEqual({
|
||||
openai: { forceReasoning: true, reasoningEffort: "xhigh" },
|
||||
})
|
||||
})
|
||||
|
||||
test("overrides forceReasoning false when reasoning should be forced", () => {
|
||||
expect(
|
||||
ProviderTransform.providerOptions(createModel(), { forceReasoning: false, reasoningEffort: "xhigh" }),
|
||||
).toEqual({
|
||||
openai: { forceReasoning: true, reasoningEffort: "xhigh" },
|
||||
})
|
||||
})
|
||||
|
||||
test("uses gateway model provider slug for gateway models", () => {
|
||||
const model = createModel({
|
||||
providerID: "vercel",
|
||||
@@ -707,7 +785,7 @@ describe("ProviderTransform.providerOptions", () => {
|
||||
})
|
||||
|
||||
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "medium" })).toEqual({
|
||||
openai: { reasoningEffort: "medium" },
|
||||
openai: { forceReasoning: true, reasoningEffort: "medium" },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2268,6 +2346,82 @@ describe("ProviderTransform.message - strip openai metadata when store=false", (
|
||||
expect(result[0].content[0].providerOptions?.openai?.reasoningEncryptedContent).toBe("encrypted")
|
||||
})
|
||||
|
||||
test("strips GitHub Copilot itemId from the copilot namespace, preserving other copilot options", () => {
|
||||
const copilotModel = {
|
||||
...openaiModel,
|
||||
id: "github-copilot/gpt-5.5",
|
||||
providerID: "github-copilot",
|
||||
api: {
|
||||
id: "gpt-5.5",
|
||||
url: "https://api.githubcopilot.com",
|
||||
npm: "@ai-sdk/github-copilot",
|
||||
},
|
||||
}
|
||||
const msgs = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "thinking...",
|
||||
providerOptions: {
|
||||
copilot: { itemId: "rs_123", reasoningEncryptedContent: "encrypted" },
|
||||
},
|
||||
},
|
||||
{
|
||||
// The stale itemId on tool-call parts is what Copilot echoes back as the
|
||||
// `function_call` item `id`, which is what the upstream connection rejects.
|
||||
type: "tool-call",
|
||||
toolCallId: "call_1",
|
||||
toolName: "bash",
|
||||
input: { command: "ls" },
|
||||
providerOptions: {
|
||||
copilot: { itemId: "fc_456", reasoningEffort: "medium" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any[]
|
||||
|
||||
const result = ProviderTransform.message(msgs, copilotModel, { store: false }) as any[]
|
||||
|
||||
expect(result[0].content[0].providerOptions?.copilot?.itemId).toBeUndefined()
|
||||
expect(result[0].content[0].providerOptions?.copilot?.reasoningEncryptedContent).toBe("encrypted")
|
||||
expect(result[0].content[1].providerOptions?.copilot?.itemId).toBeUndefined()
|
||||
expect(result[0].content[1].providerOptions?.copilot?.reasoningEffort).toBe("medium")
|
||||
})
|
||||
|
||||
test("leaves a stray openai namespace on a Copilot model untouched, since Copilot's Responses model only reads the copilot namespace", () => {
|
||||
const copilotModel = {
|
||||
...openaiModel,
|
||||
id: "github-copilot/gpt-5.5",
|
||||
providerID: "github-copilot",
|
||||
api: {
|
||||
id: "gpt-5.5",
|
||||
url: "https://api.githubcopilot.com",
|
||||
npm: "@ai-sdk/github-copilot",
|
||||
},
|
||||
}
|
||||
const msgs = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Hello",
|
||||
providerOptions: {
|
||||
openai: { itemId: "msg_456" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any[]
|
||||
|
||||
const result = ProviderTransform.message(msgs, copilotModel, { store: false }) as any[]
|
||||
|
||||
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_456")
|
||||
})
|
||||
|
||||
test("preserves metadata for openai package when store is true", () => {
|
||||
const msgs = [
|
||||
{
|
||||
@@ -3290,6 +3444,27 @@ describe("ProviderTransform.variants", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("anthropic sonnet 5 returns adaptive thinking options with xhigh", () => {
|
||||
const model = createMockModel({
|
||||
id: "anthropic/claude-sonnet-5",
|
||||
providerID: "gateway",
|
||||
api: {
|
||||
id: "anthropic/claude-sonnet-5",
|
||||
url: "https://gateway.ai",
|
||||
npm: "@ai-sdk/gateway",
|
||||
},
|
||||
})
|
||||
const result = ProviderTransform.variants(model)
|
||||
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
|
||||
expect(result.high).toEqual({
|
||||
thinking: {
|
||||
type: "adaptive",
|
||||
display: "summarized",
|
||||
},
|
||||
effort: "high",
|
||||
})
|
||||
})
|
||||
|
||||
test("anthropic opus 4.6 omits display so it keeps the summarized default", () => {
|
||||
const model = createMockModel({
|
||||
id: "anthropic/claude-opus-4-6",
|
||||
@@ -3877,6 +4052,12 @@ describe("ProviderTransform.variants", () => {
|
||||
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||
},
|
||||
{
|
||||
name: "sonnet 5",
|
||||
apiIds: ["claude-sonnet-5", "claude-sonnet-5-20260630"],
|
||||
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||
},
|
||||
{
|
||||
name: "fable 5",
|
||||
apiIds: ["claude-fable-5"],
|
||||
@@ -3974,6 +4155,28 @@ describe("ProviderTransform.variants", () => {
|
||||
effort: "high",
|
||||
})
|
||||
})
|
||||
|
||||
test("sonnet 5 uses adaptive reasoning for Vertex model IDs", () => {
|
||||
const result = ProviderTransform.variants(
|
||||
createMockModel({
|
||||
id: "google-vertex-anthropic/claude-sonnet-5@default",
|
||||
providerID: "google-vertex-anthropic",
|
||||
api: {
|
||||
id: "claude-sonnet-5@default",
|
||||
url: "https://us-central1-aiplatform.googleapis.com",
|
||||
npm: "@ai-sdk/google-vertex/anthropic",
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
|
||||
expect(result.high).toEqual({
|
||||
thinking: {
|
||||
type: "adaptive",
|
||||
display: "summarized",
|
||||
},
|
||||
effort: "high",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("@ai-sdk/amazon-bedrock", () => {
|
||||
@@ -4047,6 +4250,28 @@ describe("ProviderTransform.variants", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("anthropic sonnet 5 returns adaptive reasoning options with xhigh", () => {
|
||||
const result = ProviderTransform.variants(
|
||||
createMockModel({
|
||||
id: "bedrock/anthropic-claude-sonnet-5",
|
||||
providerID: "bedrock",
|
||||
api: {
|
||||
id: "anthropic.claude-sonnet-5",
|
||||
url: "https://bedrock.amazonaws.com",
|
||||
npm: "@ai-sdk/amazon-bedrock",
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
|
||||
expect(result.high).toEqual({
|
||||
reasoningConfig: {
|
||||
type: "adaptive",
|
||||
maxReasoningEffort: "high",
|
||||
display: "summarized",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => {
|
||||
const model = createMockModel({
|
||||
id: "bedrock/llama-4",
|
||||
@@ -4229,6 +4454,12 @@ describe("ProviderTransform.variants", () => {
|
||||
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
},
|
||||
{
|
||||
name: "sonnet 5",
|
||||
apiIds: ["anthropic--claude-sonnet-5", "anthropic--claude-5-sonnet"],
|
||||
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
},
|
||||
]) {
|
||||
for (const apiId of testCase.apiIds) {
|
||||
test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => {
|
||||
|
||||
Reference in New Issue
Block a user