chore: merge dev into v2 (#36770)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Nabs <nabil@instafork.com> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com> Co-authored-by: Mark <geraint0923@users.noreply.github.com>
This commit is contained in:
parent
26e27c7a7f
commit
634386fe0f
116 changed files with 3764 additions and 1016 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.18",
|
||||
"version": "1.17.20",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.112",
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/azure": "3.0.88",
|
||||
"@ai-sdk/cerebras": "2.0.60",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
"@ai-sdk/google-vertex": "4.0.128",
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
"@ai-sdk/openai": "3.0.84",
|
||||
"@ai-sdk/openai-compatible": "2.0.41",
|
||||
"@ai-sdk/perplexity": "3.0.26",
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.10.0",
|
||||
"gitlab-ai-provider": "6.11.1",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
|
|
|
|||
|
|
@ -348,6 +348,18 @@ const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpCl
|
|||
return yield* fetchOrgs(account.url, accessToken)
|
||||
})
|
||||
|
||||
const remove = Effect.fn("Account.remove")(function* (accountID: AccountID) {
|
||||
const active = yield* repo.active()
|
||||
yield* repo.remove(accountID)
|
||||
if (Option.isNone(active) || active.value.id !== accountID) return
|
||||
|
||||
const next = (yield* orgsByAccount()).flatMap((group) =>
|
||||
group.orgs.map((org) => ({ accountID: group.account.id, orgID: org.id })),
|
||||
)[0]
|
||||
if (!next) return
|
||||
yield* repo.use(next.accountID, Option.some(next.orgID))
|
||||
})
|
||||
|
||||
const config = Effect.fn("Account.config")(function* (accountID: AccountID, orgID: OrgID) {
|
||||
const resolved = yield* resolveAccess(accountID)
|
||||
if (Option.isNone(resolved)) return Option.none()
|
||||
|
|
@ -445,7 +457,7 @@ const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpCl
|
|||
activeOrg,
|
||||
list: repo.list,
|
||||
orgsByAccount,
|
||||
remove: repo.remove,
|
||||
remove,
|
||||
use: repo.use,
|
||||
orgs,
|
||||
config,
|
||||
|
|
|
|||
|
|
@ -283,8 +283,10 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
|||
return Object.fromEntries(
|
||||
Object.entries(provider.models)
|
||||
.filter(([, model]) => {
|
||||
if (model.options.reasoningMode === "pro") return false
|
||||
if (ALLOWED_MODELS.has(model.api.id)) return true
|
||||
if (DISALLOWED_MODELS.has(model.api.id)) return false
|
||||
if (model.api.id === "gpt-5.6") return false
|
||||
const match = model.api.id.match(/^gpt-(\d+\.\d+)/)
|
||||
return match ? parseFloat(match[1]) > 5.4 : false
|
||||
})
|
||||
|
|
@ -416,6 +418,7 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
|||
|
||||
const requestInit = {
|
||||
...init,
|
||||
body: init?.body,
|
||||
headers,
|
||||
}
|
||||
if (websocketFetch && parsed.pathname.endsWith("/responses")) return websocketFetch(url, requestInit)
|
||||
|
|
|
|||
|
|
@ -1266,6 +1266,14 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|||
// @ts-expect-error dead V1 expects raw model IDs inside normalized ModelsDev data.
|
||||
const id = `${model.id}-${mode}`
|
||||
const base = fromModelsDevModel(provider, model)
|
||||
const providerOptions = (
|
||||
opts as {
|
||||
readonly provider?: {
|
||||
readonly body?: Record<string, unknown>
|
||||
readonly headers?: Record<string, string>
|
||||
}
|
||||
}
|
||||
).provider
|
||||
models[id] = {
|
||||
...base,
|
||||
id: ModelV2.ID.make(id),
|
||||
|
|
@ -1273,18 +1281,8 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|||
name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
|
||||
// @ts-expect-error dead V1 expects raw mode costs inside normalized ModelsDev data.
|
||||
cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
|
||||
// @ts-expect-error dead V1 expects raw mode provider bodies inside normalized ModelsDev data.
|
||||
options: opts.provider?.body
|
||||
? Object.fromEntries(
|
||||
// @ts-expect-error dead V1 expects raw mode provider bodies inside normalized ModelsDev data.
|
||||
Object.entries(opts.provider.body).map(([k, v]) => [
|
||||
k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
|
||||
v,
|
||||
]),
|
||||
)
|
||||
: base.options,
|
||||
// @ts-expect-error dead V1 expects raw mode headers inside normalized ModelsDev data.
|
||||
headers: opts.provider?.headers ?? base.headers,
|
||||
options: modeOptions(base, providerOptions?.body),
|
||||
headers: providerOptions?.headers ?? base.headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1298,6 +1296,17 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|||
}
|
||||
}
|
||||
|
||||
function modeOptions(model: Model, body: Record<string, unknown> | undefined) {
|
||||
if (!body) return model.options
|
||||
const options = Object.fromEntries(
|
||||
Object.entries(body).map(([key, value]) => [key.replace(/_([a-z])/g, (_, char) => char.toUpperCase()), value]),
|
||||
)
|
||||
const reasoning = body.reasoning
|
||||
if (model.api.npm !== "@ai-sdk/openai" || !isRecord(reasoning) || typeof reasoning.mode !== "string") return options
|
||||
const { reasoning: _, ...rest } = options
|
||||
return { ...rest, reasoningMode: reasoning.mode }
|
||||
}
|
||||
|
||||
function modelSuggestions(provider: Info | undefined, modelID: ModelV2.ID, enableExperimentalModels: boolean) {
|
||||
const available = provider
|
||||
? Object.keys(provider.models).filter((id) => {
|
||||
|
|
|
|||
|
|
@ -761,7 +761,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||
}
|
||||
|
||||
case "@ai-sdk/gateway":
|
||||
if (model.id.includes("anthropic")) {
|
||||
if (model.api.id.includes("anthropic")) {
|
||||
if (adaptiveEfforts) {
|
||||
return Object.fromEntries(
|
||||
adaptiveEfforts.map((effort) => [
|
||||
|
|
@ -794,8 +794,8 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||
},
|
||||
}
|
||||
}
|
||||
if (model.id.includes("google")) {
|
||||
if (id.includes("2.5")) {
|
||||
if (model.api.id.includes("google")) {
|
||||
if (model.api.id.includes("2.5")) {
|
||||
return {
|
||||
high: {
|
||||
thinkingConfig: {
|
||||
|
|
@ -806,7 +806,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||
max: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: googleThinkingBudgetMax(id),
|
||||
thinkingBudget: googleThinkingBudgetMax(model.api.id.toLowerCase()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -1104,7 +1104,8 @@ export function options(input: {
|
|||
input.model.providerID === "openai" ||
|
||||
input.model.api.npm === "@ai-sdk/openai" ||
|
||||
input.model.api.npm === "@ai-sdk/github-copilot" ||
|
||||
input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle"
|
||||
input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle" ||
|
||||
input.model.api.npm === "@ai-sdk/xai"
|
||||
) {
|
||||
result["store"] = false
|
||||
}
|
||||
|
|
@ -1259,7 +1260,8 @@ export function smallOptions(model: Provider.Model) {
|
|||
if (
|
||||
model.providerID === "openai" ||
|
||||
model.api.npm === "@ai-sdk/openai" ||
|
||||
model.api.npm === "@ai-sdk/github-copilot"
|
||||
model.api.npm === "@ai-sdk/github-copilot" ||
|
||||
model.api.npm === "@ai-sdk/xai"
|
||||
) {
|
||||
const base = { store: false }
|
||||
return mergeDeep(base, small)
|
||||
|
|
|
|||
|
|
@ -173,6 +173,53 @@ it.live("orgsByAccount groups orgs per account", () =>
|
|||
}),
|
||||
)
|
||||
|
||||
it.live("remove switches to another org when the active account is removed", () =>
|
||||
Effect.gen(function* () {
|
||||
const first = AccountID.make("user-1")
|
||||
const second = AccountID.make("user-2")
|
||||
|
||||
yield* AccountRepo.Service.use((r) =>
|
||||
r.persistAccount({
|
||||
id: first,
|
||||
email: "one@example.com",
|
||||
url: "https://one.example.com",
|
||||
accessToken: AccessToken.make("at_1"),
|
||||
refreshToken: RefreshToken.make("rt_1"),
|
||||
expiry: Date.now() + outsideEagerRefreshWindow,
|
||||
orgID: Option.some(OrgID.make("org-1")),
|
||||
}),
|
||||
)
|
||||
|
||||
yield* AccountRepo.Service.use((r) =>
|
||||
r.persistAccount({
|
||||
id: second,
|
||||
email: "two@example.com",
|
||||
url: "https://two.example.com",
|
||||
accessToken: AccessToken.make("at_2"),
|
||||
refreshToken: RefreshToken.make("rt_2"),
|
||||
expiry: Date.now() + outsideEagerRefreshWindow,
|
||||
orgID: Option.some(OrgID.make("org-2")),
|
||||
}),
|
||||
)
|
||||
|
||||
const client = HttpClient.make((req) =>
|
||||
Effect.succeed(
|
||||
req.url === "https://one.example.com/api/orgs" ? json(req, [org("org-1", "One")]) : json(req, [], 404),
|
||||
),
|
||||
)
|
||||
|
||||
yield* Account.use.remove(second).pipe(Effect.provide(live(client)))
|
||||
|
||||
const active = yield* AccountRepo.use.active()
|
||||
expect(Option.getOrThrow(active)).toEqual(
|
||||
expect.objectContaining({
|
||||
id: first,
|
||||
active_org_id: OrgID.make("org-1"),
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("token refresh persists the new token", () =>
|
||||
Effect.gen(function* () {
|
||||
const id = AccountID.make("user-1")
|
||||
|
|
|
|||
|
|
@ -149,16 +149,32 @@ describe("plugin.codex", () => {
|
|||
await enabled.dispose?.()
|
||||
})
|
||||
|
||||
test("uses Codex context limits for OAuth GPT models", async () => {
|
||||
test("filters unsupported modes and uses Codex context limits for OAuth GPT models", async () => {
|
||||
const hooks = await CodexAuthPlugin({} as never)
|
||||
const limit = { context: 1_050_000, input: 922_000, output: 128_000 }
|
||||
const provider = {
|
||||
models: Object.fromEntries(
|
||||
["gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].map((id) => [
|
||||
id,
|
||||
{ id, api: { id }, limit, cost: {} },
|
||||
]),
|
||||
),
|
||||
models: {
|
||||
...Object.fromEntries(
|
||||
["gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.7-pro"].map((id) => [
|
||||
id,
|
||||
{ id, api: { id }, limit, cost: {}, options: {} },
|
||||
]),
|
||||
),
|
||||
"gpt-5.4-pro": {
|
||||
id: "gpt-5.4-pro",
|
||||
api: { id: "gpt-5.4" },
|
||||
limit,
|
||||
cost: {},
|
||||
options: { reasoningMode: "pro" },
|
||||
},
|
||||
"gpt-5.6-sol-high": {
|
||||
id: "gpt-5.6-sol-high",
|
||||
api: { id: "gpt-5.6-sol" },
|
||||
limit,
|
||||
cost: {},
|
||||
options: { reasoningEffort: "high" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const models = await hooks.provider!.models!(provider as never, { auth: { type: "oauth" } } as never)
|
||||
|
|
@ -168,6 +184,9 @@ describe("plugin.codex", () => {
|
|||
expect(models["gpt-5.6-sol"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 })
|
||||
expect(models["gpt-5.6-terra"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 })
|
||||
expect(models["gpt-5.6-luna"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 })
|
||||
expect(models["gpt-5.4-pro"]).toBeUndefined()
|
||||
expect(models["gpt-5.7-pro"]).toBeDefined()
|
||||
expect(models["gpt-5.6-sol-high"]).toBeDefined()
|
||||
expect(await hooks.provider!.models!(provider as never, { auth: { type: "api" } } as never)).toBe(
|
||||
provider.models as never,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1339,16 +1339,17 @@ it.instance(
|
|||
},
|
||||
)
|
||||
|
||||
test("mode cost preserves over-200k pricing from base model", () => {
|
||||
test("mode options and cost are derived from the base model", () => {
|
||||
const provider = {
|
||||
id: "openai",
|
||||
name: "OpenAI",
|
||||
env: [],
|
||||
npm: "@ai-sdk/openai",
|
||||
api: "https://api.openai.com/v1",
|
||||
models: {
|
||||
"gpt-5.4": {
|
||||
id: "gpt-5.4",
|
||||
name: "GPT-5.4",
|
||||
"gpt-5.6-sol": {
|
||||
id: "gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
family: "gpt",
|
||||
release_date: "2026-03-05",
|
||||
attachment: true,
|
||||
|
|
@ -1384,18 +1385,29 @@ test("mode cost preserves over-200k pricing from base model", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
pro: {
|
||||
provider: {
|
||||
body: {
|
||||
reasoning: { mode: "pro" },
|
||||
service_tier: "priority",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ModelsDevProvider
|
||||
|
||||
const model = Provider.fromModelsDevProvider(provider).models["gpt-5.4-fast"]
|
||||
const model = Provider.fromModelsDevProvider(provider).models["gpt-5.6-sol-fast"]
|
||||
expect(model.cost.input).toEqual(5)
|
||||
expect(model.cost.output).toEqual(30)
|
||||
expect(model.cost.cache.read).toEqual(0.5)
|
||||
expect(model.cost.cache.write).toEqual(0)
|
||||
expect(model.options["serviceTier"]).toEqual("priority")
|
||||
const pro = Provider.fromModelsDevProvider(provider).models["gpt-5.6-sol-pro"]
|
||||
expect(pro.api.id).toEqual("gpt-5.6-sol")
|
||||
expect(pro.options).toEqual({ reasoningMode: "pro", serviceTier: "priority" })
|
||||
expect(model.cost.experimentalOver200K).toEqual({
|
||||
input: 5,
|
||||
output: 22.5,
|
||||
|
|
|
|||
|
|
@ -155,6 +155,43 @@ describe("ProviderTransform.options - setCacheKey", () => {
|
|||
expect(result.store).toBe(false)
|
||||
})
|
||||
|
||||
test("should set store=false for xAI provider by default", () => {
|
||||
const xaiModel = {
|
||||
...mockModel,
|
||||
providerID: "xai",
|
||||
api: {
|
||||
id: "grok-4",
|
||||
url: "https://api.x.ai",
|
||||
npm: "@ai-sdk/xai",
|
||||
},
|
||||
}
|
||||
const result = ProviderTransform.options({
|
||||
model: xaiModel,
|
||||
sessionID,
|
||||
providerOptions: {},
|
||||
})
|
||||
expect(result.store).toBe(false)
|
||||
expect(result.promptCacheKey).toBe(sessionID)
|
||||
})
|
||||
|
||||
test("should set store=false for xAI SDK regardless of provider ID", () => {
|
||||
const xaiModel = {
|
||||
...mockModel,
|
||||
providerID: "custom-xai",
|
||||
api: {
|
||||
id: "grok-4",
|
||||
url: "https://api.x.ai",
|
||||
npm: "@ai-sdk/xai",
|
||||
},
|
||||
}
|
||||
const result = ProviderTransform.options({
|
||||
model: xaiModel,
|
||||
sessionID,
|
||||
providerOptions: {},
|
||||
})
|
||||
expect(result.store).toBe(false)
|
||||
})
|
||||
|
||||
test("should set store=false for azure provider by default", () => {
|
||||
const azureModel = {
|
||||
...mockModel,
|
||||
|
|
@ -3372,6 +3409,42 @@ describe("ProviderTransform.variants", () => {
|
|||
})
|
||||
|
||||
describe("@ai-sdk/gateway", () => {
|
||||
test("configured anthropic aliases route by the API ID", () => {
|
||||
const model = createMockModel({
|
||||
id: "my-claude",
|
||||
providerID: "gateway",
|
||||
api: {
|
||||
id: "anthropic/claude-sonnet-4-6",
|
||||
url: "https://gateway.ai",
|
||||
npm: "@ai-sdk/gateway",
|
||||
},
|
||||
})
|
||||
const result = ProviderTransform.variants(model)
|
||||
expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
|
||||
expect(result.high).toEqual({
|
||||
thinking: {
|
||||
type: "adaptive",
|
||||
},
|
||||
effort: "high",
|
||||
})
|
||||
})
|
||||
|
||||
test("configured google aliases route by the API ID", () => {
|
||||
const model = createMockModel({
|
||||
id: "my-gemini",
|
||||
providerID: "gateway",
|
||||
api: {
|
||||
id: "google/gemini-2.5-pro",
|
||||
url: "https://gateway.ai",
|
||||
npm: "@ai-sdk/gateway",
|
||||
},
|
||||
})
|
||||
expect(ProviderTransform.variants(model)).toEqual({
|
||||
high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } },
|
||||
max: { thinkingConfig: { includeThoughts: true, thinkingBudget: 32_768 } },
|
||||
})
|
||||
})
|
||||
|
||||
test("anthropic sonnet 4.6 models return adaptive thinking options", () => {
|
||||
const model = createMockModel({
|
||||
id: "anthropic/claude-sonnet-4-6",
|
||||
|
|
|
|||
|
|
@ -1095,7 +1095,7 @@ describe("session.llm.stream", () => {
|
|||
const agent = {
|
||||
name: "test",
|
||||
mode: "primary",
|
||||
options: {},
|
||||
options: { reasoningMode: "pro" },
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
temperature: 0.2,
|
||||
} satisfies Agent.Info
|
||||
|
|
@ -1126,6 +1126,7 @@ describe("session.llm.stream", () => {
|
|||
expect(body.model).toBe(resolved.api.id)
|
||||
expect(body.stream).toBe(true)
|
||||
expect((body.reasoning as { effort?: string } | undefined)?.effort).toBe("high")
|
||||
expect((body.reasoning as { mode?: string } | undefined)?.mode).toBe("pro")
|
||||
|
||||
const maxTokens = body.max_output_tokens as number | undefined
|
||||
expect(maxTokens).toBe(undefined) // match codex cli behavior
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue