chore: sync native provider stack

This commit is contained in:
Shoubhit Dash 2026-06-25 00:24:57 +05:30
commit 315ba3720e
29 changed files with 249 additions and 787 deletions

View file

@ -2,12 +2,12 @@ export * as Catalog from "./catalog"
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
import { ModelV2 } from "./model"
import { ModelRequest } from "./model-request"
import { ProviderV2 } from "./provider"
import { EventV2 } from "./event"
import { Policy } from "./policy"
import { State } from "./state"
import { Integration } from "./integration"
import { ProviderOverlay } from "./provider-overlay"
export type ProviderRecord = {
provider: ProviderV2.MutableInfo
@ -83,26 +83,27 @@ export const layer = Layer.effect(
...provider.api,
id: model.api.id,
url: model.api.url ?? provider.api.url,
settings: ModelRequest.mergeRecords(provider.api.settings, model.api.settings),
settings: ProviderOverlay.mergeRecords(provider.api.settings, model.api.settings),
}
: model.api.type === "native" && provider.api.type === "native" && !model.api.url
? {
...model.api,
package: model.api.package ?? provider.api.package,
url: provider.api.url,
settings: ModelRequest.mergeRecords(provider.api.settings, model.api.settings),
settings: ProviderOverlay.mergeRecords(provider.api.settings, model.api.settings),
}
: model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url
? {
...model.api,
url: provider.api.url,
settings: ModelRequest.mergeRecords(provider.api.settings, model.api.settings),
settings: ProviderOverlay.mergeRecords(provider.api.settings, model.api.settings),
}
: model.api.type === "aisdk" && provider.api.type === "aisdk"
? { ...model.api, settings: ModelRequest.mergeRecords(provider.api.settings, model.api.settings) }
? { ...model.api, settings: ProviderOverlay.mergeRecords(provider.api.settings, model.api.settings) }
: model.api
const request = {
...ModelRequest.merge({ ...provider.request, generation: {}, options: {} }, model.request),
headers: ProviderOverlay.mergeHeaders(provider.request.headers, model.request.headers),
body: ProviderOverlay.mergeRecords(provider.request.body, model.request.body),
variant: model.request.variant,
}
return ModelV2.Info.make({

View file

@ -4,8 +4,8 @@ import { define } from "../../plugin/internal"
import { Effect } from "effect"
import { Config } from "../../config"
import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider"
import { ProviderOverlay } from "../../provider-overlay"
export const Plugin = define({
id: "config-provider",
@ -57,7 +57,7 @@ export const Plugin = define({
catalog.provider.update(providerID, (provider) => {
if (item.name !== undefined) provider.name = item.name
if (item.package !== undefined) {
const settings = ModelRequest.mergeRecords(provider.api.settings, item.settings)
const settings = ProviderOverlay.mergeRecords(provider.api.settings, item.settings)
const url =
item.settings && Object.hasOwn(item.settings, "baseURL")
? typeof settings.baseURL === "string"
@ -68,7 +68,7 @@ export const Plugin = define({
? { type: "aisdk", package: item.package, ...(url === undefined ? {} : { url }), settings }
: { type: "native", package: item.package, ...(url === undefined ? {} : { url }), settings }
} else if (item.settings !== undefined) {
provider.api.settings = ModelRequest.mergeRecords(provider.api.settings, item.settings)
provider.api.settings = ProviderOverlay.mergeRecords(provider.api.settings, item.settings)
if (Object.hasOwn(item.settings, "baseURL")) {
provider.api.url =
typeof provider.api.settings.baseURL === "string" ? provider.api.settings.baseURL : undefined
@ -82,11 +82,9 @@ export const Plugin = define({
provider.api = { ...provider.api, type: "native", settings: provider.api.settings ?? {} }
}
}
ModelRequest.assign(provider.request, { headers: item.headers, body: item.body })
ProviderOverlay.assign(provider.request, { headers: item.headers, body: item.body })
})
const providerApi = catalog.provider.get(providerID)?.provider.api
const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
for (const [id, config] of Object.entries(item.models ?? {})) {
const modelKey = `${providerID}/${id}`
if (config.aiSDK !== undefined) modelAiSDK.set(modelKey, config.aiSDK)
@ -97,7 +95,7 @@ export const Plugin = define({
if (config.package !== undefined) {
const aiSDK =
modelAiSDK.get(modelKey) ?? providerAiSDK.get(providerID) ?? providerApi?.type === "aisdk"
const settings = ModelRequest.mergeRecords(model.api.settings, config.settings)
const settings = ProviderOverlay.mergeRecords(model.api.settings, config.settings)
const url =
config.settings && Object.hasOwn(config.settings, "baseURL")
? typeof settings.baseURL === "string"
@ -120,7 +118,7 @@ export const Plugin = define({
settings,
}
} else if (config.settings !== undefined) {
model.api.settings = ModelRequest.mergeRecords(model.api.settings, config.settings)
model.api.settings = ProviderOverlay.mergeRecords(model.api.settings, config.settings)
if (Object.hasOwn(config.settings, "baseURL")) {
model.api.url =
typeof model.api.settings.baseURL === "string" ? model.api.settings.baseURL : undefined
@ -134,9 +132,6 @@ export const Plugin = define({
model.api = { ...model.api, type: "native", settings: model.api.settings ?? {} }
}
}
const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
const aiSDK =
modelAiSDK.get(modelKey) ?? providerAiSDK.get(providerID) ?? providerApi?.type === "aisdk"
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
@ -145,12 +140,7 @@ export const Plugin = define({
}
}
if (config.headers !== undefined || config.body !== undefined) {
ModelRequest.assign(model.request, {
headers: config.headers,
...(aiSDK
? ModelRequest.normalizeAiSdkOptions(packageName, config.body ?? {})
: { body: config.body }),
})
ProviderOverlay.assign(model.request, { headers: config.headers, body: config.body })
}
if (config.variant !== undefined) model.request.variant = config.variant
if (config.variants !== undefined) {
@ -162,18 +152,11 @@ export const Plugin = define({
settings: {},
headers: {},
body: {},
generation: {},
options: {},
}
model.variants.push(existing)
}
existing.settings = ModelRequest.mergeRecords(existing.settings, variant.settings)
ModelRequest.assign(existing, {
headers: variant.headers,
...(aiSDK
? ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {})
: { body: variant.body }),
})
existing.settings = ProviderOverlay.mergeRecords(existing.settings, variant.settings)
ProviderOverlay.assign(existing, { headers: variant.headers, body: variant.body })
}
}
if (config.cost !== undefined) {

View file

@ -1,127 +0,0 @@
export * as ModelRequest from "./model-request"
import { ModelRequest } from "@opencode-ai/schema/model-request"
export const Generation = ModelRequest.Generation
export type Generation = ModelRequest.Generation
export const Request = ModelRequest.Request
export type Request = ModelRequest.Request
interface MutableRequest {
headers: Record<string, string>
body: Record<string, unknown>
generation?: Record<string, unknown>
options?: Record<string, unknown>
}
const generationKeys = new Map<string, keyof Generation>([
["maxOutputTokens", "maxTokens"],
["maxTokens", "maxTokens"],
["temperature", "temperature"],
["topP", "topP"],
["topK", "topK"],
["frequencyPenalty", "frequencyPenalty"],
["presencePenalty", "presencePenalty"],
["seed", "seed"],
["stopSequences", "stop"],
["stop", "stop"],
])
interface Profile {
readonly namespace: string
readonly semantics: ReadonlyMap<string, string>
}
const profiles = new Map<string, Profile>([
[
"@ai-sdk/openai",
{
namespace: "openai",
semantics: new Map([
["store", "store"],
["promptCacheKey", "promptCacheKey"],
["reasoningEffort", "reasoningEffort"],
["reasoningSummary", "reasoningSummary"],
["include", "include"],
["textVerbosity", "textVerbosity"],
["serviceTier", "serviceTier"],
["service_tier", "serviceTier"],
]),
},
],
[
"@ai-sdk/openai-compatible",
{
namespace: "openai",
semantics: new Map([
["store", "store"],
["promptCacheKey", "promptCacheKey"],
["reasoningEffort", "reasoningEffort"],
["reasoning_effort", "reasoningEffort"],
]),
},
],
["@ai-sdk/anthropic", { namespace: "anthropic", semantics: new Map([["thinking", "thinking"]]) }],
])
export const namespace = (packageName: string) => profiles.get(packageName)?.namespace
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
export const mergeRecords = (...items: ReadonlyArray<Readonly<Record<string, unknown>> | undefined>) => {
const result: Record<string, unknown> = {}
for (const item of items) {
for (const [key, value] of Object.entries(item ?? {})) {
result[key] = isRecord(result[key]) && isRecord(value) ? mergeRecords(result[key], value) : value
}
}
return result
}
export const mergeHeaders = (...items: ReadonlyArray<Readonly<Record<string, string>> | undefined>) => {
const result = new Map<string, readonly [string, string]>()
for (const item of items) {
for (const entry of Object.entries(item ?? {})) result.set(entry[0].toLowerCase(), entry)
}
return Object.fromEntries(result.values())
}
export const merge = (base: Request, override: Partial<Request>) => ({
headers: mergeHeaders(base.headers, override.headers),
body: mergeRecords(base.body, override.body),
generation: { ...base.generation, ...override.generation },
options: { ...base.options, ...override.options },
})
export const assign = (target: MutableRequest, override: Partial<Request>) => {
const headers = mergeHeaders(target.headers, override.headers)
Object.keys(target.headers).forEach((key) => delete target.headers[key])
Object.assign(target.headers, headers)
const body = mergeRecords(target.body, override.body)
Object.keys(target.body).forEach((key) => delete target.body[key])
Object.assign(target.body, body)
Object.assign((target.generation ??= {}), override.generation)
Object.assign((target.options ??= {}), override.options)
}
/** Partitions AI-SDK-shaped request options before they enter the Catalog. */
export function normalizeAiSdkOptions(packageName: string | undefined, input: Readonly<Record<string, unknown>>) {
const generation: Record<string, number | ReadonlyArray<string>> = {}
const options: Record<string, unknown> = {}
const body: Record<string, unknown> = {}
const semantics = profiles.get(packageName ?? "")?.semantics
for (const [key, value] of Object.entries(input)) {
const generationKey = generationKeys.get(key)
if (generationKey === "stop" && Array.isArray(value) && value.every((item) => typeof item === "string"))
generation[generationKey] = value
else if (generationKey !== undefined && generationKey !== "stop" && typeof value === "number")
generation[generationKey] = value
else if (semantics?.has(key)) options[semantics.get(key)!] = value
else body[key] = value
}
return { generation, options, body }
}

View file

@ -29,6 +29,7 @@ import { CommandPlugin } from "./command"
import { ModelsDevPlugin } from "./models-dev"
import { ProviderPlugins } from "./provider"
import { SkillPlugin } from "./skill"
import { VariantPlugin } from "./variant"
export type Requirements =
| AgentV2.Service
@ -107,12 +108,13 @@ export const locationLayer = Layer.effectDiscard(
yield* add(CommandPlugin.Plugin)
yield* add(SkillPlugin.Plugin)
yield* add(ModelsDevPlugin)
yield* add(ConfigProviderPlugin.Plugin)
yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin)
for (const item of ProviderPlugins) yield* add(item)
yield* add(ConfigExternalPlugin.Plugin)
yield* add(ConfigProviderPlugin.Plugin)
yield* add(VariantPlugin.Plugin)
}).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
}),
).pipe(

View file

@ -2,7 +2,6 @@ import { define } from "./internal"
import { Effect, Stream } from "effect"
import { EventV2 } from "../event"
import { ModelV2 } from "../model"
import { ModelRequest } from "../model-request"
import { ModelsDev } from "../models-dev"
import { ProviderV2 } from "../provider"
@ -38,15 +37,12 @@ function cost(input: ModelsDev.Model["cost"]) {
]
}
function variants(model: ModelsDev.Model, packageName?: string) {
return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => {
const request = ModelRequest.normalizeAiSdkOptions(packageName, item.provider?.body ?? {})
return {
id: ModelV2.VariantID.make(id),
headers: { ...(item.provider?.headers ?? {}) },
...request,
}
})
function variants(model: ModelsDev.Model) {
return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => ({
id: ModelV2.VariantID.make(id),
headers: { ...(item.provider?.headers ?? {}) },
body: { ...(item.provider?.body ?? {}) },
}))
}
export const ModelsDevPlugin = define({
@ -115,7 +111,7 @@ export const ModelsDevPlugin = define({
input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])],
}
draft.variants = variants(model, model.provider?.npm ?? item.npm)
draft.variants = variants(model)
draft.time.released = released(model.release_date)
draft.cost = cost(model.cost)
draft.status = model.status ?? "active"

View file

@ -8,9 +8,9 @@ import { EventV2 } from "../../event"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider"
import { ConfigProviderV1 } from "../../v1/config/provider"
import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options"
import { ConfigV1 } from "../../v1/config/config"
const defaultServer = "https://console.opencode.ai"
@ -142,15 +142,14 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
const packageName = config.provider?.npm ?? item.npm
ModelRequest.assign(model.request, {
headers: config.headers,
...ModelRequest.normalizeAiSdkOptions(packageName, withoutCredentials(config.options)),
})
const lowerer = ConfigProviderOptionsV1.get(packageName)
Object.assign(model.request.headers, config.headers)
Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options)))
if (config.variants !== undefined) {
model.variants = Object.entries(config.variants).map(([id, options]) => ({
id: ModelV2.VariantID.make(id),
headers: { ...(options.headers ?? {}) },
...ModelRequest.normalizeAiSdkOptions(packageName, withoutCredentials(options)),
body: lowerer.request(withoutCredentials(options)),
}))
}
if (config.release_date !== undefined) {

View file

@ -0,0 +1,39 @@
export * as VariantPlugin from "./variant"
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect } from "effect"
import { define } from "./internal"
export const Plugin = define({
id: "variant",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((catalog) => {
for (const record of catalog.provider.list()) {
for (const model of record.models.values()) {
catalog.model.update(model.providerID, model.id, (draft) => {
const generated = generate(draft)
if (generated.length === 0) return
const explicit = new Map(draft.variants.map((variant) => [variant.id, variant]))
const generatedIDs = new Set(generated.map((variant) => variant.id))
draft.variants = [
...generated.map((variant) => explicit.get(variant.id) ?? variant),
...draft.variants.filter((variant) => !generatedIDs.has(variant.id)),
]
})
}
}
})
}),
})
export function generate(model: ModelV2Info): ModelV2Info["variants"] {
if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return []
const ids = `${model.id} ${model.api.id}`.toLowerCase()
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
return ["high", "max"].map((id) => ({
id,
headers: {},
body: { reasoning_effort: id },
}))
}

View file

@ -0,0 +1,34 @@
export * as ProviderOverlay from "./provider-overlay"
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
export const mergeRecords = (...items: ReadonlyArray<Readonly<Record<string, unknown>> | undefined>) => {
const result: Record<string, unknown> = {}
for (const item of items) {
for (const [key, value] of Object.entries(item ?? {})) {
result[key] = isRecord(result[key]) && isRecord(value) ? mergeRecords(result[key], value) : value
}
}
return result
}
export const mergeHeaders = (...items: ReadonlyArray<Readonly<Record<string, string>> | undefined>) => {
const result = new Map<string, readonly [string, string]>()
for (const item of items) {
for (const entry of Object.entries(item ?? {})) result.set(entry[0].toLowerCase(), entry)
}
return Object.fromEntries(result.values())
}
export const assign = (
target: { headers: Record<string, string>; body: Record<string, unknown> },
overlay: { readonly headers?: Readonly<Record<string, string>>; readonly body?: Readonly<Record<string, unknown>> },
) => {
const headers = mergeHeaders(target.headers, overlay.headers)
Object.keys(target.headers).forEach((key) => delete target.headers[key])
Object.assign(target.headers, headers)
const body = mergeRecords(target.body, overlay.body)
Object.keys(target.body).forEach((key) => delete target.body[key])
Object.assign(target.body, body)
}

View file

@ -11,8 +11,8 @@ import { Catalog } from "../../catalog"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider"
import { ProviderOverlay } from "../../provider-overlay"
import { SessionSchema } from "../schema"
export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelectedError>()(
@ -88,8 +88,6 @@ const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
}
const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
const options = model.request.options ?? {}
const namespace = model.api.type === "aisdk" ? ModelRequest.namespace(model.api.package) : undefined
const body = model.request.body
const httpBody = Object.hasOwn(body, "apiKey")
? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
@ -98,8 +96,6 @@ const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
provider: model.providerID,
endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
headers: model.request.headers,
generation: model.request.generation,
providerOptions: namespace && Object.keys(options).length > 0 ? { [namespace]: options } : undefined,
http: { body: httpBody },
limits: { context: model.limit.context, output: model.limit.output },
})
@ -123,12 +119,12 @@ const withVariant = (
variant
? produce(model, (draft) => {
if (variant.settings !== undefined) {
draft.api.settings = ModelRequest.mergeRecords(draft.api.settings, variant.settings)
draft.api.settings = ProviderOverlay.mergeRecords(draft.api.settings, variant.settings)
if (Object.hasOwn(variant.settings, "baseURL")) {
draft.api.url = typeof draft.api.settings.baseURL === "string" ? draft.api.settings.baseURL : undefined
}
}
ModelRequest.assign(draft.request, variant)
ProviderOverlay.assign(draft.request, variant)
})
: model,
)

View file

@ -6,7 +6,6 @@ import { ConfigMCPV1 } from "./mcp"
import { ConfigPermissionV1 } from "./permission"
import { ConfigProviderV1 } from "./provider"
import { ConfigProviderOptionsV1 } from "./provider-options"
import { ModelRequest } from "../../model-request"
const keys = new Set([
"logLevel",
@ -191,11 +190,7 @@ function migrateProvider(info: ConfigProviderV1.Info) {
function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) {
const packageID = info.provider?.npm ?? packageName
const lowerer = ConfigProviderOptionsV1.get(packageID)
const ingest = (options: Readonly<Record<string, unknown>>) => {
const request = ModelRequest.normalizeAiSdkOptions(packageID, options)
return { ...lowerer.request(request.body), ...request.generation, ...request.options }
}
const request = info.options && ingest(info.options)
const request = info.options && lowerer.request(info.options)
const costs = info.cost && [
{
input: info.cost.input,
@ -231,7 +226,7 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
info.variants &&
Object.entries(info.variants).map(([id, options]) => ({
id,
body: ingest(options),
body: lowerer.request(options),
})),
cost: costs,
disabled: info.status === "deprecated" ? true : undefined,