Răsfoiți Sursa

feat(core): flatten provider config and load native packages (#35563)

Co-authored-by: Dax Raad <d@ironbay.co>
Shoubhit Dash 1 lună în urmă
părinte
comite
e57d9ca390
100 a modificat fișierele cu 2426 adăugiri și 1780 ștergeri
  1. 2 5
      packages/cli/src/mini/catalog.shared.ts
  2. 24 72
      packages/client/src/promise/generated/types.ts
  3. 458 16
      packages/core/src/aisdk.ts
  4. 5 25
      packages/core/src/catalog.ts
  5. 2 1
      packages/core/src/config.ts
  6. 2 2
      packages/core/src/config/agent.ts
  7. 2 2
      packages/core/src/config/command.ts
  8. 38 0
      packages/core/src/config/model.ts
  9. 14 14
      packages/core/src/config/plugin/agent.ts
  10. 6 8
      packages/core/src/config/plugin/command.ts
  11. 21 26
      packages/core/src/config/plugin/provider.ts
  12. 16 26
      packages/core/src/config/provider.ts
  13. 2 12
      packages/core/src/model.ts
  14. 29 38
      packages/core/src/plugin/models-dev.ts
  15. 11 9
      packages/core/src/plugin/provider/amazon-bedrock.ts
  16. 7 4
      packages/core/src/plugin/provider/anthropic.ts
  17. 21 10
      packages/core/src/plugin/provider/azure.ts
  18. 4 3
      packages/core/src/plugin/provider/cerebras.ts
  19. 11 8
      packages/core/src/plugin/provider/cloudflare-workers-ai.ts
  20. 3 4
      packages/core/src/plugin/provider/github-copilot.ts
  21. 10 12
      packages/core/src/plugin/provider/gitlab.ts
  22. 22 20
      packages/core/src/plugin/provider/google-vertex.ts
  23. 5 5
      packages/core/src/plugin/provider/kilo.ts
  24. 10 6
      packages/core/src/plugin/provider/llmgateway.ts
  25. 10 6
      packages/core/src/plugin/provider/nvidia.ts
  26. 4 4
      packages/core/src/plugin/provider/openai.ts
  27. 21 23
      packages/core/src/plugin/provider/opencode.ts
  28. 4 4
      packages/core/src/plugin/provider/openrouter.ts
  29. 1 1
      packages/core/src/plugin/provider/sap-ai-core.ts
  30. 4 4
      packages/core/src/plugin/provider/vercel.ts
  31. 1 1
      packages/core/src/plugin/provider/xai.ts
  32. 9 5
      packages/core/src/plugin/provider/zenmux.ts
  33. 15 11
      packages/core/src/plugin/variant.ts
  34. 114 13
      packages/core/src/provider.ts
  35. 4 1
      packages/core/src/session/runner/llm.ts
  36. 146 66
      packages/core/src/session/runner/model.ts
  37. 5 5
      packages/core/src/session/runner/to-llm-message.ts
  38. 44 37
      packages/core/src/v1/config/migrate.ts
  39. 16 212
      packages/core/src/v1/config/provider-options.ts
  40. 85 0
      packages/core/test/aisdk.test.ts
  41. 34 54
      packages/core/test/catalog.test.ts
  42. 2 2
      packages/core/test/config/agent.test.ts
  43. 1 2
      packages/core/test/config/command.test.ts
  44. 40 35
      packages/core/test/config/config.test.ts
  45. 28 0
      packages/core/test/config/model.test.ts
  46. 20 196
      packages/core/test/config/provider-options.test.ts
  47. 24 25
      packages/core/test/config/provider.test.ts
  48. 6 6
      packages/core/test/location-layer.test.ts
  49. 4 6
      packages/core/test/plugin/fixtures/variant-source-plugin.ts
  50. 11 18
      packages/core/test/plugin/host.ts
  51. 14 23
      packages/core/test/plugin/models-dev.test.ts
  52. 10 6
      packages/core/test/plugin/provider-alibaba.test.ts
  53. 49 63
      packages/core/test/plugin/provider-amazon-bedrock.test.ts
  54. 11 11
      packages/core/test/plugin/provider-anthropic.test.ts
  55. 27 21
      packages/core/test/plugin/provider-azure-cognitive-services.test.ts
  56. 37 28
      packages/core/test/plugin/provider-azure.test.ts
  57. 10 19
      packages/core/test/plugin/provider-cerebras.test.ts
  58. 22 15
      packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts
  59. 31 42
      packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts
  60. 8 4
      packages/core/test/plugin/provider-cohere.test.ts
  61. 13 11
      packages/core/test/plugin/provider-deepinfra.test.ts
  62. 17 9
      packages/core/test/plugin/provider-dynamic.test.ts
  63. 8 8
      packages/core/test/plugin/provider-gateway.test.ts
  64. 26 13
      packages/core/test/plugin/provider-github-copilot.test.ts
  65. 23 18
      packages/core/test/plugin/provider-gitlab.test.ts
  66. 29 22
      packages/core/test/plugin/provider-google-vertex-anthropic.test.ts
  67. 64 71
      packages/core/test/plugin/provider-google-vertex.test.ts
  68. 27 4
      packages/core/test/plugin/provider-google.test.ts
  69. 12 11
      packages/core/test/plugin/provider-groq.test.ts
  70. 18 29
      packages/core/test/plugin/provider-kilo.test.ts
  71. 8 14
      packages/core/test/plugin/provider-llmgateway.test.ts
  72. 12 7
      packages/core/test/plugin/provider-mistral.test.ts
  73. 12 25
      packages/core/test/plugin/provider-nvidia.test.ts
  74. 10 5
      packages/core/test/plugin/provider-openai-compatible.test.ts
  75. 16 12
      packages/core/test/plugin/provider-openai.test.ts
  76. 38 36
      packages/core/test/plugin/provider-opencode.test.ts
  77. 9 7
      packages/core/test/plugin/provider-openrouter.test.ts
  78. 10 5
      packages/core/test/plugin/provider-perplexity.test.ts
  79. 2 1
      packages/core/test/plugin/provider-sap-ai-core.test.ts
  80. 12 6
      packages/core/test/plugin/provider-snowflake-cortex.test.ts
  81. 13 10
      packages/core/test/plugin/provider-togetherai.test.ts
  82. 10 5
      packages/core/test/plugin/provider-venice.test.ts
  83. 9 10
      packages/core/test/plugin/provider-vercel.test.ts
  84. 11 6
      packages/core/test/plugin/provider-xai.test.ts
  85. 14 23
      packages/core/test/plugin/provider-zenmux.test.ts
  86. 5 11
      packages/core/test/plugin/variant.test.ts
  87. 2 3
      packages/core/test/session-instructions.test.ts
  88. 33 3
      packages/core/test/session-runner-message.test.ts
  89. 246 90
      packages/core/test/session-runner-model.test.ts
  90. 0 4
      packages/core/test/shared-schema.test.ts
  91. 3 0
      packages/llm/package.json
  92. 2 0
      packages/llm/src/index.ts
  93. 16 0
      packages/llm/src/provider-package.ts
  94. 26 1
      packages/llm/src/providers/amazon-bedrock.ts
  95. 15 1
      packages/llm/src/providers/anthropic.ts
  96. 17 0
      packages/llm/src/providers/openai-compatible.ts
  97. 37 1
      packages/llm/src/providers/openai.ts
  98. 2 0
      packages/llm/src/providers/openai/chat.ts
  99. 2 0
      packages/llm/src/providers/openai/responses.ts
  100. 0 1
      packages/llm/test/exports.test.ts

+ 2 - 5
packages/cli/src/mini/catalog.shared.ts

@@ -84,7 +84,7 @@ export function runProviders(providers: CurrentProvider[], models: CurrentModel[
       cost: defaultCost(model),
       limit: model.limit,
       status: model.status,
-      variants: Object.fromEntries(model.variants.map((variant) => [variant.id, {}])),
+      variants: Object.fromEntries((model.variants ?? []).map((variant) => [variant.id, {}])),
     }
     grouped.set(provider.id, provider)
   }
@@ -141,10 +141,7 @@ export async function loadRunCommands(sdk: OpenCodeClient, directory: string): P
     sdk.command.list(location(directory)),
     sdk.skill.list(location(directory)),
   ])
-  return [
-    ...commands.data.map(runCommand),
-    ...skills.data.filter((skill) => skill.slash !== false).map(runSkill),
-  ]
+  return [...commands.data.map(runCommand), ...skills.data.filter((skill) => skill.slash !== false).map(runSkill)]
 }
 
 export async function loadRunReferences(sdk: OpenCodeClient, directory: string): Promise<RunReference[]> {

+ 24 - 72
packages/client/src/promise/generated/types.ts

@@ -2009,39 +2009,24 @@ export type ModelListOutput = {
   }
   readonly data: ReadonlyArray<{
     readonly id: string
+    readonly modelID: string
     readonly providerID: string
     readonly family?: string
     readonly name: string
-    readonly api:
-      | {
-          readonly id: string
-          readonly type: "aisdk"
-          readonly package: string
-          readonly url?: string
-          readonly settings?: { readonly [x: string]: JsonValue }
-        }
-      | {
-          readonly id: string
-          readonly type: "native"
-          readonly url?: string
-          readonly settings: { readonly [x: string]: JsonValue }
-        }
+    readonly package?: string
+    readonly settings?: { readonly [x: string]: JsonValue }
+    readonly headers?: { readonly [x: string]: string }
+    readonly body?: { readonly [x: string]: JsonValue }
     readonly capabilities: {
       readonly tools: boolean
       readonly input: ReadonlyArray<string>
       readonly output: ReadonlyArray<string>
     }
-    readonly request: {
-      readonly settings: { readonly [x: string]: JsonValue }
-      readonly headers: { readonly [x: string]: string }
-      readonly body: { readonly [x: string]: JsonValue }
-      readonly variant?: string
-    }
     readonly variants: ReadonlyArray<{
       readonly id: string
-      readonly settings: { readonly [x: string]: JsonValue }
-      readonly headers: { readonly [x: string]: string }
-      readonly body: { readonly [x: string]: JsonValue }
+      readonly settings?: { readonly [x: string]: JsonValue }
+      readonly headers?: { readonly [x: string]: string }
+      readonly body?: { readonly [x: string]: JsonValue }
     }>
     readonly time: { readonly released: number }
     readonly cost: ReadonlyArray<{
@@ -2070,39 +2055,24 @@ export type ModelDefaultOutput = {
   }
   readonly data: {
     readonly id: string
+    readonly modelID: string
     readonly providerID: string
     readonly family?: string
     readonly name: string
-    readonly api:
-      | {
-          readonly id: string
-          readonly type: "aisdk"
-          readonly package: string
-          readonly url?: string
-          readonly settings?: { readonly [x: string]: JsonValue }
-        }
-      | {
-          readonly id: string
-          readonly type: "native"
-          readonly url?: string
-          readonly settings: { readonly [x: string]: JsonValue }
-        }
+    readonly package?: string
+    readonly settings?: { readonly [x: string]: JsonValue }
+    readonly headers?: { readonly [x: string]: string }
+    readonly body?: { readonly [x: string]: JsonValue }
     readonly capabilities: {
       readonly tools: boolean
       readonly input: ReadonlyArray<string>
       readonly output: ReadonlyArray<string>
     }
-    readonly request: {
-      readonly settings: { readonly [x: string]: JsonValue }
-      readonly headers: { readonly [x: string]: string }
-      readonly body: { readonly [x: string]: JsonValue }
-      readonly variant?: string
-    }
     readonly variants: ReadonlyArray<{
       readonly id: string
-      readonly settings: { readonly [x: string]: JsonValue }
-      readonly headers: { readonly [x: string]: string }
-      readonly body: { readonly [x: string]: JsonValue }
+      readonly settings?: { readonly [x: string]: JsonValue }
+      readonly headers?: { readonly [x: string]: string }
+      readonly body?: { readonly [x: string]: JsonValue }
     }>
     readonly time: { readonly released: number }
     readonly cost: ReadonlyArray<{
@@ -2150,19 +2120,10 @@ export type ProviderListOutput = {
     readonly integrationID?: string
     readonly name: string
     readonly disabled?: boolean
-    readonly api:
-      | {
-          readonly type: "aisdk"
-          readonly package: string
-          readonly url?: string
-          readonly settings?: { readonly [x: string]: JsonValue }
-        }
-      | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
-    readonly request: {
-      readonly settings: { readonly [x: string]: JsonValue }
-      readonly headers: { readonly [x: string]: string }
-      readonly body: { readonly [x: string]: JsonValue }
-    }
+    readonly package: string
+    readonly settings?: { readonly [x: string]: JsonValue }
+    readonly headers?: { readonly [x: string]: string }
+    readonly body?: { readonly [x: string]: JsonValue }
   }>
 }
 
@@ -2184,19 +2145,10 @@ export type ProviderGetOutput = {
     readonly integrationID?: string
     readonly name: string
     readonly disabled?: boolean
-    readonly api:
-      | {
-          readonly type: "aisdk"
-          readonly package: string
-          readonly url?: string
-          readonly settings?: { readonly [x: string]: JsonValue }
-        }
-      | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
-    readonly request: {
-      readonly settings: { readonly [x: string]: JsonValue }
-      readonly headers: { readonly [x: string]: string }
-      readonly body: { readonly [x: string]: JsonValue }
-    }
+    readonly package: string
+    readonly settings?: { readonly [x: string]: JsonValue }
+    readonly headers?: { readonly [x: string]: string }
+    readonly body?: { readonly [x: string]: JsonValue }
   }
 }
 

+ 458 - 16
packages/core/src/aisdk.ts

@@ -1,13 +1,43 @@
 export * as AISDK from "./aisdk"
 
 import { makeLocationNode } from "./effect/app-node"
-import type { LanguageModelV3 } from "@ai-sdk/provider"
-import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
+import type {
+  JSONSchema7,
+  JSONValue,
+  LanguageModelV3,
+  LanguageModelV3CallOptions,
+  LanguageModelV3FunctionTool,
+  LanguageModelV3Message,
+  LanguageModelV3Prompt,
+  LanguageModelV3StreamPart,
+  LanguageModelV3ToolChoice,
+  SharedV3ProviderOptions,
+} from "@ai-sdk/provider"
+import {
+  FinishReason,
+  InvalidProviderOutputReason,
+  LLMEvent,
+  LLMError,
+  Model,
+  ProviderID,
+  ProviderMetadata,
+  ToolResultValue,
+  UnknownProviderReason,
+  type ContentPart,
+  type LLMRequest,
+  type ToolDefinition,
+  type UsageInput,
+} from "@opencode-ai/llm"
+import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/llm/route"
+import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
 import { ModelV2 } from "./model"
 import { ProviderV2 } from "./provider"
 import { State } from "./state"
 
 type SDK = any
+type UserContent = Extract<LanguageModelV3Message, { role: "user" }>["content"]
+type AssistantContent = Extract<LanguageModelV3Message, { role: "assistant" }>["content"]
+type ToolResultContent = Extract<AssistantContent[number], { type: "tool-result" }>
 
 export interface SDKEvent {
   readonly model: ModelV2.Info
@@ -74,10 +104,10 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
 function prepareOptions(model: ModelV2.Info, pkg: string) {
   const options: Record<string, any> = {
     name: model.providerID,
-    ...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}),
-    ...model.request.body,
+    ...(model.settings ?? {}),
+    headers: model.headers,
+    body: model.body,
   }
-  if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url
 
   const customFetch = options.fetch
   const chunkTimeout = options.chunkTimeout
@@ -110,6 +140,13 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
       }
     }
 
+    if (typeof opts.body === "string" && model.body !== undefined) {
+      const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body))
+      if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) {
+        opts.body = JSON.stringify(ProviderV2.mergeOverlay(decoded, model.body))
+      }
+    }
+
     const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
       ...opts,
       timeout: false,
@@ -142,17 +179,29 @@ export interface Interface {
   readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
   readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
   readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
+  readonly model: (model: ModelV2.Info) => Effect.Effect<Model, InitError>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
 
-const layer = Layer.effect(
+export const locationLayer = Layer.effect(
   Service,
   Effect.gen(function* () {
     let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = []
     let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
     const languages = new Map<string, LanguageModelV3>()
     const sdks = new Map<string, SDK>()
+    const functionIDs = new WeakMap<object, number>()
+    let nextFunctionID = 0
+    const cacheKey = (input: unknown) =>
+      JSON.stringify(input, (_key, value: unknown) => {
+        if (typeof value !== "function") return value
+        const existing = functionIDs.get(value)
+        if (existing !== undefined) return `function:${existing}`
+        const id = nextFunctionID++
+        functionIDs.set(value, id)
+        return `function:${id}`
+      }) ?? ""
 
     const register = <Event>(
       hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
@@ -196,24 +245,36 @@ const layer = Layer.effect(
       runSDK: (event) => run(sdkHooks, event),
       runLanguage: (event) => run(languageHooks, event),
       language: Effect.fn("AISDK.language")(function* (model) {
-        const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
+        const key = cacheKey({
+          providerID: model.providerID,
+          id: model.id,
+          modelID: model.modelID,
+          package: model.package,
+          settings: model.settings,
+          headers: model.headers,
+          body: model.body,
+          limit: model.limit,
+        })
         const existing = languages.get(key)
         if (existing) return existing
-        if (model.api.type !== "aisdk")
+        if (!ProviderV2.isAISDK(model.package))
           return yield* new InitError({
             providerID: model.providerID,
-            cause: new Error(`Unsupported api ${model.api.type}`),
+            cause: new Error(`Unsupported package ${model.package}`),
           })
 
-        const options = prepareOptions(model, model.api.package)
-        const sdkKey = JSON.stringify({
+        const packageName = ProviderV2.packageName(model.package) ?? ""
+        const options = prepareOptions(model, packageName)
+        const sdkKey = cacheKey({
           providerID: model.providerID,
-          api: model.api,
-          options,
+          package: packageName,
+          settings: model.settings,
+          headers: model.headers,
+          body: model.body,
         })
         const sdk =
           sdks.get(sdkKey) ??
-          (yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk
+          (yield* service.runSDK({ model, package: packageName, options }).pipe(initError(model.providerID))).sdk
         if (!sdk)
           return yield* new InitError({
             providerID: model.providerID,
@@ -221,15 +282,396 @@ const layer = Layer.effect(
           })
         sdks.set(sdkKey, sdk)
         const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
-        const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
+        const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.modelID ?? model.id)).pipe(
           initError(model.providerID),
         )
         languages.set(key, language)
         return language
       }),
+      model: Effect.fn("AISDK.model")(function* (model) {
+        return modelFromLanguage(model, yield* service.language(model))
+      }),
     })
     return service
   }),
 )
 
-export const node = makeLocationNode({ service: Service, layer, deps: [] })
+export const defaultLayer = locationLayer
+
+function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
+  const settings = requestSettings(info.settings)
+  const optionKey = providerOptionKey(ProviderV2.packageName(info.package), info.providerID)
+  const route: AnyRoute = {
+    id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
+    provider: ProviderID.make(info.providerID),
+    protocol: "ai-sdk",
+    endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
+    auth: Auth.none,
+    transport: {
+      id: "ai-sdk",
+      prepare: (input) => Effect.succeed(input.body),
+      frames: () => Stream.empty,
+    },
+    defaults: {
+      headers: info.headers,
+      http:
+        info.body === undefined && info.headers === undefined
+          ? undefined
+          : { body: info.body === undefined ? undefined : { ...info.body }, headers: info.headers },
+      limits: { context: info.limit.context, output: info.limit.output },
+      providerOptions: settings === undefined ? undefined : { [optionKey]: settings },
+    },
+    body: {
+      schema: Schema.Unknown,
+      from: (request) => Effect.succeed(callOptions(request)),
+    },
+    with: () => route,
+    model: (input) => Model.make({ ...input, provider: "provider" in input ? input.provider : info.providerID, route }),
+    prepareTransport: (body) => Effect.succeed(body),
+    streamPrepared: (prepared) => streamLanguage(language, prepared as LanguageModelV3CallOptions),
+  }
+  return Model.make({ id: info.modelID ?? info.id, provider: info.providerID, route })
+}
+
+function providerOptionKey(packageName: string | undefined, providerID: ProviderV2.ID) {
+  if (packageName === "@ai-sdk/google") return "google"
+  if (packageName === "@ai-sdk/google-vertex") return "vertex"
+  if (packageName === "@ai-sdk/google-vertex/anthropic") return "anthropic"
+  if (packageName === "@ai-sdk/amazon-bedrock" || packageName === "@ai-sdk/amazon-bedrock/mantle") return "bedrock"
+  if (packageName === "@ai-sdk/azure") return "azure"
+  if (packageName === "@openrouter/ai-sdk-provider") return "openrouter"
+  if (packageName?.startsWith("@ai-sdk/")) return packageName.slice("@ai-sdk/".length)
+  return providerID
+}
+
+function requestSettings(settings: Readonly<Record<string, unknown>> | undefined) {
+  if (settings === undefined) return undefined
+  const result = Object.fromEntries(
+    Object.entries(settings).filter(
+      ([key]) => !["apiKey", "authToken", "baseURL", "chunkTimeout", "fetch", "timeout"].includes(key),
+    ),
+  )
+  return Object.keys(result).length === 0 ? undefined : result
+}
+
+function callOptions(request: LLMRequest): LanguageModelV3CallOptions {
+  return {
+    prompt: prompt(request),
+    maxOutputTokens: request.generation?.maxTokens ?? request.model.route.defaults.limits?.output,
+    temperature: request.generation?.temperature,
+    stopSequences: request.generation?.stop === undefined ? undefined : [...request.generation.stop],
+    topP: request.generation?.topP,
+    topK: request.generation?.topK,
+    presencePenalty: request.generation?.presencePenalty,
+    frequencyPenalty: request.generation?.frequencyPenalty,
+    seed: request.generation?.seed,
+    responseFormat: responseFormat(request),
+    tools: request.tools.map(tool),
+    toolChoice: toolChoice(request.toolChoice),
+    headers: request.http?.headers,
+    providerOptions: providerOptions(request.providerOptions),
+  }
+}
+
+function prompt(request: LLMRequest): LanguageModelV3Prompt {
+  const system = request.system
+    .map((part) => part.text)
+    .filter(Boolean)
+    .join("\n\n")
+  const messages = request.messages.flatMap(message)
+  if (!system.length) return messages
+  return [{ role: "system", content: system }, ...messages]
+}
+
+function message(input: LLMRequest["messages"][number]): LanguageModelV3Message[] {
+  switch (input.role) {
+    case "system":
+      return [{ role: "system", content: input.content.flatMap(text).join("\n\n") }]
+    case "user":
+      return [{ role: "user", content: input.content.flatMap(userPart) }]
+    case "assistant":
+      return [{ role: "assistant", content: input.content.flatMap(assistantPart) }]
+    case "tool": {
+      const content = input.content.flatMap(toolResultPart)
+      return content.length ? [{ role: "tool", content }] : []
+    }
+  }
+}
+
+function text(part: ContentPart) {
+  return part.type === "text" ? [part.text] : []
+}
+
+function userPart(part: ContentPart): UserContent {
+  if (part.type === "text") return [{ type: "text", text: part.text }]
+  if (part.type === "media")
+    return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
+  return []
+}
+
+function assistantPart(part: ContentPart): AssistantContent {
+  switch (part.type) {
+    case "text":
+      return [{ type: "text", text: part.text }]
+    case "media":
+      return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
+    case "reasoning":
+      return [{ type: "reasoning", text: part.text }]
+    case "tool-call":
+      return [
+        {
+          type: "tool-call",
+          toolCallId: part.id,
+          toolName: part.name,
+          input: part.input,
+          providerExecuted: part.providerExecuted,
+        },
+      ]
+    case "tool-result":
+      return toolResultPart(part)
+  }
+}
+
+function toolResultPart(part: ContentPart): ToolResultContent[] {
+  if (part.type !== "tool-result") return []
+  return [
+    {
+      type: "tool-result",
+      toolCallId: part.id,
+      toolName: part.name,
+      output: toolOutput(part.result),
+    },
+  ]
+}
+
+function toolOutput(result: ToolResultValue) {
+  switch (result.type) {
+    case "text":
+    case "error":
+      return { type: "text" as const, value: messageValue(result.value) }
+  }
+  return { type: "json" as const, value: jsonValue(result.value) }
+}
+
+function tool(input: ToolDefinition): LanguageModelV3FunctionTool {
+  return {
+    type: "function",
+    name: input.name,
+    description: input.description,
+    inputSchema: input.inputSchema as JSONSchema7,
+  }
+}
+
+function toolChoice(input: LLMRequest["toolChoice"]): LanguageModelV3ToolChoice | undefined {
+  if (!input) return undefined
+  if (input.type === "tool") return input.name === undefined ? undefined : { type: "tool", toolName: input.name }
+  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 {
+  if (!input) return undefined
+  return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)]))
+}
+
+function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallOptions) {
+  const state = { step: 0, toolNames: {} as Record<string, string> }
+  return Stream.concat(
+    Stream.make(LLMEvent.stepStart({ index: state.step })),
+    Stream.unwrap(
+      Effect.tryPromise({
+        try: () => language.doStream(options),
+        catch: (error) => llmError("doStream", error),
+      }).pipe(
+        Effect.map((result) =>
+          Stream.fromReadableStream({
+            evaluate: () => result.stream,
+            onError: (error) => llmError("readStream", error),
+          }).pipe(
+            Stream.mapEffect((event) => streamPartEvents(state, event)),
+            Stream.flatMap((events) => Stream.fromIterable(events)),
+          ),
+        ),
+      ),
+    ),
+  )
+}
+
+function streamPartEvents(
+  state: { step: number; toolNames: Record<string, string> },
+  event: LanguageModelV3StreamPart,
+): Effect.Effect<ReadonlyArray<LLMEvent>, LLMError> {
+  switch (event.type) {
+    case "stream-start":
+    case "response-metadata":
+    case "raw":
+    case "file":
+    case "source":
+    case "tool-approval-request":
+      return Effect.succeed([])
+    case "text-start":
+      return Effect.succeed([
+        LLMEvent.textStart({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
+      ])
+    case "text-delta":
+      return Effect.succeed([
+        LLMEvent.textDelta({
+          id: event.id,
+          text: event.delta,
+          providerMetadata: providerMetadata(event.providerMetadata),
+        }),
+      ])
+    case "text-end":
+      return Effect.succeed([
+        LLMEvent.textEnd({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
+      ])
+    case "reasoning-start":
+      return Effect.succeed([
+        LLMEvent.reasoningStart({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
+      ])
+    case "reasoning-delta":
+      return Effect.succeed([
+        LLMEvent.reasoningDelta({
+          id: event.id,
+          text: event.delta,
+          providerMetadata: providerMetadata(event.providerMetadata),
+        }),
+      ])
+    case "reasoning-end":
+      return Effect.succeed([
+        LLMEvent.reasoningEnd({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
+      ])
+    case "tool-input-start":
+      state.toolNames[event.id] = event.toolName
+      return Effect.succeed([
+        LLMEvent.toolInputStart({
+          id: event.id,
+          name: event.toolName,
+          providerMetadata: providerMetadata(event.providerMetadata),
+        }),
+      ])
+    case "tool-input-delta":
+      return Effect.succeed([
+        LLMEvent.toolInputDelta({ id: event.id, name: state.toolNames[event.id] ?? "unknown", text: event.delta }),
+      ])
+    case "tool-input-end":
+      return Effect.succeed([
+        LLMEvent.toolInputEnd({
+          id: event.id,
+          name: state.toolNames[event.id] ?? "unknown",
+          providerMetadata: providerMetadata(event.providerMetadata),
+        }),
+      ])
+    case "tool-call":
+      state.toolNames[event.toolCallId] = event.toolName
+      return Effect.succeed([
+        LLMEvent.toolCall({
+          id: event.toolCallId,
+          name: event.toolName,
+          input: parseToolInput(event.input),
+          providerExecuted: event.providerExecuted,
+          providerMetadata: providerMetadata(event.providerMetadata),
+        }),
+      ])
+    case "tool-result":
+      delete state.toolNames[event.toolCallId]
+      return Effect.succeed([
+        LLMEvent.toolResult({
+          id: event.toolCallId,
+          name: event.toolName,
+          result: ToolResultValue.make(event.result, event.isError ? "error" : "json"),
+          providerExecuted: true,
+          providerMetadata: providerMetadata(event.providerMetadata),
+        }),
+      ])
+    case "finish":
+      return Effect.succeed([
+        LLMEvent.stepFinish({
+          index: state.step++,
+          reason: finishReason(event.finishReason),
+          usage: usage(event.usage),
+          providerMetadata: providerMetadata(event.providerMetadata),
+        }),
+        LLMEvent.finish({
+          reason: finishReason(event.finishReason),
+          usage: usage(event.usage),
+          providerMetadata: providerMetadata(event.providerMetadata),
+        }),
+      ])
+    case "error":
+      return Effect.fail(llmError("stream", event.error))
+  }
+}
+
+function usage(input: Extract<LanguageModelV3StreamPart, { type: "finish" }>["usage"]): UsageInput | undefined {
+  const output = {
+    inputTokens: input.inputTokens.total,
+    nonCachedInputTokens: input.inputTokens.noCache,
+    cacheReadInputTokens: input.inputTokens.cacheRead,
+    cacheWriteInputTokens: input.inputTokens.cacheWrite,
+    outputTokens: input.outputTokens.total,
+    reasoningTokens: input.outputTokens.reasoning,
+    totalTokens:
+      input.inputTokens.total === undefined || input.outputTokens.total === undefined
+        ? undefined
+        : input.inputTokens.total + input.outputTokens.total,
+  }
+  return Object.values(output).some((value) => value !== undefined) ? output : undefined
+}
+
+function finishReason(value: unknown): FinishReason {
+  return Schema.is(FinishReason)(value) ? value : "unknown"
+}
+
+function providerMetadata(value: unknown) {
+  return Schema.is(ProviderMetadata)(value) ? value : undefined
+}
+
+function parseToolInput(value: string) {
+  try {
+    return JSON.parse(value) as unknown
+  } catch {
+    return value
+  }
+}
+
+function jsonObject(input: Record<string, unknown>) {
+  return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonValue(value)]))
+}
+
+function jsonValue(input: unknown): JSONValue {
+  try {
+    const encoded = JSON.stringify(input)
+    return encoded === undefined ? null : (JSON.parse(encoded) as JSONValue)
+  } catch {
+    return messageValue(input)
+  }
+}
+
+function messageValue(input: unknown) {
+  if (typeof input === "string") return input
+  try {
+    return JSON.stringify(input) ?? String(input)
+  } catch {
+    return String(input)
+  }
+}
+
+function llmError(method: string, error: unknown) {
+  const reason =
+    error instanceof LLMError
+      ? new InvalidProviderOutputReason({ message: error.message })
+      : new UnknownProviderReason({ message: error instanceof Error ? error.message : String(error) })
+  return new LLMError({
+    module: "AISDK",
+    method,
+    reason,
+  })
+}
+
+export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] })

+ 5 - 25
packages/core/src/catalog.ts

@@ -66,39 +66,21 @@ const layer = Layer.effect(
 
     const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
       if (provider.disabled) return false
-      if (typeof provider.request.body.apiKey === "string") return true
+      if (typeof provider.settings?.apiKey === "string") return true
       if (integration?.connections.length) return true
       return provider.integrationID === undefined && !integration
     }
 
     const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
-      const api =
-        model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0
-          ? { ...provider.api, id: model.api.id }
-          : model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url
-            ? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } }
-            : model.api.type === "aisdk" && provider.api.type === "aisdk"
-              ? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
-              : model.api
-      const request = {
-        settings: { ...provider.request.settings, ...model.request.settings },
-        headers: { ...provider.request.headers, ...model.request.headers },
-        body: { ...provider.request.body, ...model.request.body },
-        variant: model.request.variant,
-      }
       return ModelV2.Info.make({
         ...model,
-        api,
-        request,
+        package: model.package ?? provider.package,
+        settings: ProviderV2.mergeOverlay(provider.settings, model.settings),
+        headers: ProviderV2.mergeHeaders(provider.headers, model.headers),
+        body: ProviderV2.mergeOverlay(provider.body, model.body),
       })
     }
 
-    const normalizeApi = (item: ProviderV2.MutableInfo | ModelV2.MutableInfo) => {
-      if (typeof item.request.body.baseURL !== "string") return
-      item.api.url = item.request.body.baseURL
-      delete item.request.body.baseURL
-    }
-
     const state = State.create<Data, Draft>({
       initial: () => ({ providers: new Map() }),
       draft: (draft) => {
@@ -116,7 +98,6 @@ const layer = Layer.effect(
                 draft.providers.set(providerID, current)
               }
               fn(current.provider)
-              normalizeApi(current.provider)
             },
             remove: (providerID) => {
               draft.providers.delete(providerID)
@@ -139,7 +120,6 @@ const layer = Layer.effect(
               fn(model)
               model.id = modelID
               model.providerID = providerID
-              normalizeApi(model)
             },
             remove: (providerID, modelID) => {
               draft.providers.get(providerID)?.models.delete(modelID)

+ 2 - 1
packages/core/src/config.ts

@@ -19,6 +19,7 @@ import { ConfigCommand } from "./config/command"
 import { ConfigFormatter } from "./config/formatter"
 import { ConfigLSP } from "./config/lsp"
 import { ConfigMCP } from "./config/mcp"
+import { ConfigModel } from "./config/model"
 import { ConfigPlugin } from "./config/plugin"
 import { ConfigProvider } from "./config/provider"
 import { ConfigReference } from "./config/reference"
@@ -35,7 +36,7 @@ export class Info extends Schema.Class<Info>("Config.Info")({
   shell: Schema.String.pipe(Schema.optional).annotate({
     description: "Default shell to use for terminal and shell tool execution",
   }),
-  model: Schema.String.pipe(Schema.optional).annotate({
+  model: ConfigModel.Selection.pipe(Schema.optional).annotate({
     description: "Default model to use when no session or agent model is selected",
   }),
   default_agent: Schema.String.pipe(Schema.optional).annotate({

+ 2 - 2
packages/core/src/config/agent.ts

@@ -3,6 +3,7 @@ export * as ConfigAgent from "./agent"
 import { Schema } from "effect"
 import { Permission } from "@opencode-ai/schema/permission"
 import { ConfigProvider } from "./provider"
+import { ConfigModel } from "./model"
 import { PositiveInt } from "../schema"
 
 export const Color = Schema.Union([
@@ -11,8 +12,7 @@ export const Color = Schema.Union([
 ])
 
 export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
-  model: Schema.String.pipe(Schema.optional),
-  variant: Schema.String.pipe(Schema.optional),
+  model: ConfigModel.Selection.pipe(Schema.optional),
   request: ConfigProvider.Request.pipe(Schema.optional),
   system: Schema.String.pipe(Schema.optional),
   description: Schema.String.pipe(Schema.optional),

+ 2 - 2
packages/core/src/config/command.ts

@@ -1,12 +1,12 @@
 export * as ConfigCommand from "./command"
 
 import { Schema } from "effect"
+import { ConfigModel } from "./model"
 
 export class Info extends Schema.Class<Info>("ConfigV2.Command")({
   template: Schema.String,
   description: Schema.String.pipe(Schema.optional),
   agent: Schema.String.pipe(Schema.optional),
-  model: Schema.String.pipe(Schema.optional),
-  variant: Schema.String.pipe(Schema.optional),
+  model: ConfigModel.Selection.pipe(Schema.optional),
   subtask: Schema.Boolean.pipe(Schema.optional),
 }) {}

+ 38 - 0
packages/core/src/config/model.ts

@@ -0,0 +1,38 @@
+export * as ConfigModel from "./model"
+
+import { Schema, SchemaGetter } from "effect"
+import { Model } from "@opencode-ai/schema/model"
+import { Provider } from "@opencode-ai/schema/provider"
+
+const ProviderID = Provider.ID.check(Schema.isPattern(/^[^/#]+$/))
+const ModelID = Model.ID.check(Schema.isPattern(/^[^#]+$/))
+const VariantID = Model.VariantID.check(Schema.isPattern(/^[^#]+$/))
+
+const Explicit = Schema.Struct({
+  providerID: ProviderID,
+  model: ModelID,
+  variant: VariantID.pipe(Schema.optional),
+})
+
+const Short = Schema.String.check(Schema.isPattern(/^[^/#]+\/[^#]+(?:#[^#]+)?$/))
+
+export interface Selection extends Schema.Schema.Type<typeof Explicit> {}
+export const Selection = Schema.Union([Short, Explicit])
+  .pipe(
+    Schema.decodeTo(Explicit, {
+      decode: SchemaGetter.transform((input) => (typeof input === "string" ? parse(input) : input)),
+      encode: SchemaGetter.passthrough({ strict: false }),
+    }),
+  )
+  .annotate({ identifier: "Config.ModelSelection" })
+
+function parse(input: string): Selection {
+  const providerEnd = input.indexOf("/")
+  const variantStart = input.lastIndexOf("#")
+  const hasVariant = variantStart > providerEnd
+  return {
+    providerID: Provider.ID.make(input.slice(0, providerEnd)),
+    model: Model.ID.make(input.slice(providerEnd + 1, hasVariant ? variantStart : undefined)),
+    ...(hasVariant ? { variant: Model.VariantID.make(input.slice(variantStart + 1)) } : {}),
+  }
+}

+ 14 - 14
packages/core/src/config/plugin/agent.ts

@@ -8,7 +8,6 @@ import { Config } from "../../config"
 import { ConfigAgent } from "../agent"
 import { ConfigMarkdown } from "../markdown"
 import { FSUtil } from "../../fs-util"
-import { ModelV2 } from "../../model"
 import { ConfigAgentV1 } from "../../v1/config/agent"
 import { ConfigMigrateV1 } from "../../v1/config/migrate"
 
@@ -76,13 +75,12 @@ export const Plugin = define({
           const exists = draft.get(agentID) !== undefined
           draft.update(agentID, (agent) => {
             if (!exists) agent.permissions.push(...global)
-            if (item.model !== undefined) {
-              const model = ModelV2.parse(item.model)
-              agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
-            }
-            if (item.variant !== undefined && agent.model !== undefined) {
-              agent.model.variant = ModelV2.VariantID.make(item.variant)
-            }
+            if (item.model !== undefined)
+              agent.model = {
+                id: item.model.model,
+                providerID: item.model.providerID,
+                ...(item.model.variant === undefined ? {} : { variant: item.model.variant }),
+              }
             if (item.request !== undefined) {
               Object.assign(agent.request.headers, item.request.headers ?? {})
               Object.assign(agent.request.body, item.request.body ?? {})
@@ -134,14 +132,16 @@ function decode(file: { directory: string; filepath: string; primary: boolean },
     .replace(/\.md$/, "")
   const body = markdown.content.trim()
   const legacy = Object.keys(markdown.data).some((key) => !agentKeys.has(key))
-  const agent = Option.getOrUndefined(
-    legacy
-      ? Option.map(
+  const agent = legacy
+    ? Option.getOrUndefined(
+        Option.map(
           decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all", propertyOrder: "original" }),
           ConfigMigrateV1.migrateAgent,
-        )
-      : decodeAgent({ ...markdown.data, system: body }, { errors: "all", propertyOrder: "original" }),
-  )
+        ),
+      )
+    : Option.getOrUndefined(
+        decodeAgent({ ...markdown.data, system: body }, { errors: "all", propertyOrder: "original" }),
+      )
   if (!agent) return
   const info = Option.getOrUndefined(
     decodeConfig({

+ 6 - 8
packages/core/src/config/plugin/command.ts

@@ -6,7 +6,6 @@ import { Effect, Option, Schema, Stream } from "effect"
 import { CommandV2 } from "../../command"
 import { Config } from "../../config"
 import { FSUtil } from "../../fs-util"
-import { ModelV2 } from "../../model"
 import { ConfigCommand } from "../command"
 import { ConfigMarkdown } from "../markdown"
 
@@ -35,13 +34,12 @@ export const Plugin = define({
             item.template = command.template
             if (command.description !== undefined) item.description = command.description
             if (command.agent !== undefined) item.agent = command.agent
-            if (command.model !== undefined) {
-              const model = ModelV2.parse(command.model)
-              item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
-            }
-            if (command.variant !== undefined && item.model !== undefined) {
-              item.model.variant = ModelV2.VariantID.make(command.variant)
-            }
+            if (command.model !== undefined)
+              item.model = {
+                id: command.model.model,
+                providerID: command.model.providerID,
+                ...(command.model.variant === undefined ? {} : { variant: command.model.variant }),
+              }
             if (command.subtask !== undefined) item.subtask = command.subtask
           })
         }

+ 21 - 26
packages/core/src/config/plugin/provider.ts

@@ -4,6 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
 import { Effect, Stream } from "effect"
 import { Config } from "../../config"
 import { ModelV2 } from "../../model"
+import { ProviderV2 } from "../../provider"
 
 export const Plugin = define({
   id: "opencode.config.provider",
@@ -39,27 +40,29 @@ export const Plugin = define({
     yield* ctx.catalog.transform((catalog) => {
       const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")
       const configuredDefault = Config.latest(loaded.entries, "model")
-      if (configuredDefault !== undefined) {
-        const model = ModelV2.parse(configuredDefault)
-        catalog.model.default.set(model.providerID, model.modelID)
-      }
+      if (configuredDefault !== undefined)
+        catalog.model.default.set(configuredDefault.providerID, configuredDefault.model)
       for (const file of files) {
         for (const [id, item] of Object.entries(file.info.providers ?? {})) {
           const providerID = id
           catalog.provider.update(providerID, (provider) => {
             if (item.name !== undefined) provider.name = item.name
-            if (item.api !== undefined) provider.api = { ...item.api }
-            if (item.request !== undefined) {
-              Object.assign(provider.request.settings, item.request.settings)
-              Object.assign(provider.request.headers, item.request.headers)
-              Object.assign(provider.request.body, item.request.body)
-            }
+            if (item.package !== undefined) provider.package = item.package
+            if (item.settings !== undefined)
+              provider.settings = ProviderV2.mergeOverlay(provider.settings, item.settings)
+            if (item.headers !== undefined) provider.headers = ProviderV2.mergeHeaders(provider.headers, item.headers)
+            if (item.body !== undefined) provider.body = ProviderV2.mergeOverlay(provider.body, item.body)
           })
           for (const [id, config] of Object.entries(item.models ?? {})) {
             catalog.model.update(providerID, id, (model) => {
               if (config.family !== undefined) model.family = config.family
               if (config.name !== undefined) model.name = config.name
-              if (config.api !== undefined) model.api = { ...model.api, ...config.api }
+              if (config.modelID !== undefined) model.modelID = config.modelID
+              if (config.package !== undefined) model.package = config.package
+              if (config.settings !== undefined)
+                model.settings = ProviderV2.mergeOverlay(model.settings, config.settings)
+              if (config.headers !== undefined) model.headers = ProviderV2.mergeHeaders(model.headers, config.headers)
+              if (config.body !== undefined) model.body = ProviderV2.mergeOverlay(model.body, config.body)
               if (config.capabilities !== undefined) {
                 model.capabilities = {
                   tools: config.capabilities.tools,
@@ -67,27 +70,19 @@ export const Plugin = define({
                   output: [...config.capabilities.output],
                 }
               }
-              if (config.request !== undefined) {
-                Object.assign(model.request.settings, config.request.settings)
-                Object.assign(model.request.headers, config.request.headers)
-                Object.assign(model.request.body, config.request.body)
-                if (config.request.variant !== undefined) model.request.variant = config.request.variant
-              }
               if (config.variants !== undefined) {
+                model.variants ??= []
                 for (const variant of config.variants) {
                   let existing = model.variants.find((item) => item.id === variant.id)
                   if (!existing) {
-                    existing = {
-                      id: variant.id,
-                      settings: {},
-                      headers: {},
-                      body: {},
-                    }
+                    existing = { id: variant.id }
                     model.variants.push(existing)
                   }
-                  Object.assign(existing.settings, variant.settings)
-                  Object.assign(existing.headers, variant.headers)
-                  Object.assign(existing.body, variant.body)
+                  if (variant.settings !== undefined)
+                    existing.settings = ProviderV2.mergeOverlay(existing.settings, variant.settings)
+                  if (variant.headers !== undefined)
+                    existing.headers = ProviderV2.mergeHeaders(existing.headers, variant.headers)
+                  if (variant.body !== undefined) existing.body = ProviderV2.mergeOverlay(existing.body, variant.body)
                 }
               }
               if (config.cost !== undefined) {

+ 16 - 26
packages/core/src/config/provider.ts

@@ -1,13 +1,19 @@
 export * as ConfigProvider from "./provider"
 
 import { Schema } from "effect"
-import { ProviderV2 } from "../provider"
 import { ModelV2 } from "../model"
 
-export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
-  settings: ProviderV2.Settings.pipe(Schema.optional),
+const JsonRecord = Schema.Record(Schema.String, Schema.Json)
+
+export const Overlays = {
+  settings: JsonRecord.pipe(Schema.optional),
   headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
-  body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
+  body: JsonRecord.pipe(Schema.optional),
+}
+
+export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
+  headers: Overlays.headers,
+  body: Overlays.body,
 }) {}
 
 class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
@@ -31,32 +37,16 @@ class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({
   output: Schema.Int.pipe(Schema.optional),
 }) {}
 
-const ModelApi = Schema.Union([
-  Schema.Struct({
-    id: ModelV2.ID.pipe(Schema.optional),
-    ...ProviderV2.AISDK.fields,
-  }),
-  Schema.Struct({
-    id: ModelV2.ID.pipe(Schema.optional),
-    ...ProviderV2.Native.fields,
-  }),
-  Schema.Struct({
-    id: ModelV2.ID,
-  }),
-])
-
 class Model extends Schema.Class<Model>("ConfigV2.Model")({
+  modelID: ModelV2.ID.pipe(Schema.optional),
   family: ModelV2.Family.pipe(Schema.optional),
   name: Schema.String.pipe(Schema.optional),
-  api: ModelApi.pipe(Schema.optional),
+  package: Schema.String.pipe(Schema.optional),
+  ...Overlays,
   capabilities: ModelV2.Capabilities.pipe(Schema.optional),
-  request: Schema.Struct({
-    ...Request.fields,
-    variant: Schema.String.pipe(Schema.optional),
-  }).pipe(Schema.optional),
   variants: Schema.Struct({
     id: ModelV2.VariantID,
-    ...Request.fields,
+    ...Overlays,
   }).pipe(Schema.Array, Schema.optional),
   cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
   disabled: Schema.Boolean.pipe(Schema.optional),
@@ -66,7 +56,7 @@ class Model extends Schema.Class<Model>("ConfigV2.Model")({
 export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
   name: Schema.String.pipe(Schema.optional),
   env: Schema.String.pipe(Schema.Array, Schema.optional),
-  api: ProviderV2.Api.pipe(Schema.optional),
-  request: Request.pipe(Schema.optional),
+  package: Schema.String.pipe(Schema.optional),
+  ...Overlays,
   models: Schema.Record(Schema.String, Model).pipe(Schema.optional),
 }) {}

+ 2 - 12
packages/core/src/model.ts

@@ -1,6 +1,6 @@
-import { Types } from "effect"
 import { Model } from "@opencode-ai/schema/model"
 import { ProviderV2 } from "./provider"
+import type { DeepMutable } from "./schema"
 
 export const ID = Model.ID
 export type ID = typeof ID.Type
@@ -20,20 +20,10 @@ export const Cost = Model.Cost
 export const Ref = Model.Ref
 export type Ref = typeof Ref.Type
 
-export const Api = Model.Api
-export type Api = Model.Api
-
 export const Info = Model.Info
 export type Info = Model.Info
 
-export type MutableRequest = ProviderV2.MutableRequest & { variant?: string }
-export type MutableVariant = ProviderV2.MutableRequest & { id: VariantID }
-
-export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request" | "variants"> & {
-  api: ProviderV2.MutableApi<Api>
-  request: MutableRequest
-  variants: MutableVariant[]
-}
+export type MutableInfo = DeepMutable<Info>
 
 export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
   const [providerID, ...modelID] = input.split("/")

+ 29 - 38
packages/core/src/plugin/models-dev.ts

@@ -72,7 +72,7 @@ function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"]
 
 const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
 
-function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): ModelV2Info["variants"] {
+function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelV2Info["variants"]> {
   const npm = model.provider?.npm ?? provider.npm
   const options = model.reasoning_options ?? []
   const effort = options.find((option) => option.type === "effort")
@@ -82,7 +82,7 @@ function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model)
       const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined
       if (id === undefined) return []
       const settings = settingsForEffort(npm, id)
-      return settings ? [{ id, settings, headers: {}, body: {} }] : []
+      return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : []
     })
   }
 
@@ -117,15 +117,18 @@ function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.
 function budgetVariants(
   npm: string | undefined,
   option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
-): ModelV2Info["variants"] {
+): NonNullable<ModelV2Info["variants"]> {
   const max = option.max
-  const high = option.max === undefined ? Math.max(option.min ?? 0, 16_000) : Math.min(Math.max(option.min ?? 0, 16_000), option.max)
+  const high =
+    option.max === undefined
+      ? Math.max(option.min ?? 0, 16_000)
+      : Math.min(Math.max(option.min ?? 0, 16_000), option.max)
   return [
     { id: "high", budget: high },
     ...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
   ].flatMap((item) => {
     const settings = settingsForBudget(npm, item.budget)
-    return settings ? [{ id: item.id, settings, headers: {}, body: {} }] : []
+    return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : []
   })
 }
 
@@ -143,12 +146,13 @@ function modeName(model: ModelsDev.Model, mode: string) {
   return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
 }
 
-function mergeVariants(model: ModelV2Info, next: ModelV2Info["variants"]) {
-  const existing = new Map(model.variants.map((variant) => [variant.id, variant]))
+function mergeVariants(model: ModelV2Info, next: NonNullable<ModelV2Info["variants"]>) {
+  const variants = model.variants ?? []
+  const existing = new Map(variants.map((variant) => [variant.id, variant]))
   const nextIDs = new Set(next.map((variant) => variant.id))
   model.variants = [
     ...next.map((variant) => existing.get(variant.id) ?? variant),
-    ...model.variants.filter((variant) => !nextIDs.has(variant.id)),
+    ...variants.filter((variant) => !nextIDs.has(variant.id)),
   ]
 }
 
@@ -159,24 +163,14 @@ function applyModel(
     readonly name?: string
     readonly cost?: ModelV2Info["cost"]
     readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
-    readonly variants?: ModelV2Info["variants"]
+    readonly variants?: NonNullable<ModelV2Info["variants"]>
   } = {},
 ) {
   draft.name = input.name ?? model.name
+  draft.modelID = model.id
   draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
-  draft.api = model.provider?.npm
-    ? {
-        id: ModelV2.ID.make(model.id),
-        type: "aisdk",
-        package: model.provider.npm,
-        url: model.provider.api,
-      }
-    : {
-        id: ModelV2.ID.make(model.id),
-        type: "native",
-        url: model.provider?.api,
-        settings: {},
-      }
+  draft.package = model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined
+  draft.settings = model.provider?.api ? { ...draft.settings, baseURL: model.provider.api } : draft.settings
   draft.capabilities = {
     tools: model.tool_call,
     input: [...(model.modalities?.input ?? [])],
@@ -184,7 +178,11 @@ function applyModel(
   }
   mergeVariants(draft, input.variants ?? [])
   draft.time.released = released(model.release_date)
-  draft.cost = input.cost ?? cost(model.cost)
+  draft.cost = (input.cost ?? cost(model.cost)).map((item) => ({
+    ...item,
+    tier: item.tier && { ...item.tier },
+    cache: { ...item.cache },
+  }))
   draft.status = model.status ?? "active"
   draft.enabled = true
   draft.limit = {
@@ -192,8 +190,8 @@ function applyModel(
     input: model.limit.input,
     output: model.limit.output,
   }
-  Object.assign(draft.request.headers, input.request?.headers ?? {})
-  Object.assign(draft.request.body, input.request?.body ?? {})
+  draft.headers = { ...draft.headers, ...input.request?.headers }
+  draft.body = { ...draft.body, ...input.request?.body }
 }
 
 export const ModelsDevPlugin = define({
@@ -222,25 +220,18 @@ export const ModelsDevPlugin = define({
         const providerID = ProviderV2.ID.make(item.id)
         catalog.provider.update(providerID, (provider) => {
           provider.name = item.name
-          provider.api = item.npm
-            ? {
-                type: "aisdk",
-                package: item.npm,
-                url: item.api,
-              }
-            : {
-                type: "native",
-                url: item.api,
-                settings: {},
-              }
+          provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
+          provider.settings = item.api ? { ...provider.settings, baseURL: item.api } : provider.settings
         })
 
         for (const model of Object.values(item.models)) {
           const baseCost = cost(model.cost)
           const variants = reasoningVariants(item, model)
-          catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants }))
+          catalog.model.update(providerID, ModelV2.ID.make(model.id), (draft) =>
+            applyModel(draft, model, { cost: baseCost, variants }),
+          )
           for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
-            catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>
+            catalog.model.update(providerID, ModelV2.ID.make(`${model.id}-${mode}`), (draft) =>
               applyModel(draft, model, {
                 name: modeName(model, mode),
                 cost: mergeCost(baseCost, options.cost),

+ 11 - 9
packages/core/src/plugin/provider/amazon-bedrock.ts

@@ -64,15 +64,14 @@ export const AmazonBedrockPlugin = define({
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
         evt.provider.update(item.provider.id, (provider) => {
-          if (provider.api.type !== "aisdk") return
-          if (typeof provider.request.body.endpoint !== "string") return
+          if (typeof provider.settings?.endpoint !== "string") return
           // The AI SDK expects a base URL, but users configure Bedrock private/VPC
           // endpoints as `endpoint`; move it into the catalog endpoint URL once.
-          provider.api.url = provider.request.body.endpoint
-          delete provider.request.body.endpoint
+          provider.settings.baseURL = provider.settings.endpoint
+          delete provider.settings.endpoint
         })
       }
     })
@@ -112,12 +111,15 @@ export const AmazonBedrockPlugin = define({
     yield* ctx.aisdk.language(
       Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
-        if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
-          evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
+        if (
+          ProviderV2.isAISDK(evt.model.package) &&
+          ProviderV2.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle"
+        ) {
+          evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id)
           return
         }
         const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
-        evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
+        evt.language = evt.sdk.languageModel(resolveModelID(evt.model.modelID ?? evt.model.id, region))
       }),
     )
   }),

+ 7 - 4
packages/core/src/plugin/provider/anthropic.ts

@@ -1,16 +1,19 @@
 import { Effect } from "effect"
 import { define } from "@opencode-ai/plugin/v2/effect/plugin"
+import { ProviderV2 } from "../../provider"
 
 export const AnthropicPlugin = define({
   id: "opencode.provider.anthropic",
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/anthropic") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue
         evt.provider.update(item.provider.id, (provider) => {
-          provider.request.headers["anthropic-beta"] =
-            "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
+          provider.headers = {
+            ...provider.headers,
+            "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
+          }
         })
       }
     })

+ 21 - 10
packages/core/src/plugin/provider/azure.ts

@@ -15,14 +15,14 @@ export const AzurePlugin = define({
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/azure") continue
-        const configured = item.provider.request.body.resourceName
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/azure") continue
+        const configured = item.provider.settings?.resourceName
         const resourceName =
           typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
         if (!resourceName) continue
         evt.provider.update(item.provider.id, (provider) => {
-          provider.request.body.resourceName = resourceName
+          provider.settings = { ...provider.settings, resourceName }
         })
       }
     })
@@ -33,7 +33,7 @@ export const AzurePlugin = define({
           if (
             !evt.options.resourceName &&
             !evt.options.baseURL &&
-            (evt.model.api.type !== "aisdk" || !evt.model.api.url)
+            (!ProviderV2.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string")
           ) {
             throw new Error(
               "AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
@@ -47,7 +47,11 @@ export const AzurePlugin = define({
     yield* ctx.aisdk.language(
       Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.azure) return
-        evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
+        evt.language = selectLanguage(
+          evt.sdk,
+          evt.model.modelID ?? evt.model.id,
+          Boolean(evt.options.useCompletionUrls),
+        )
       }),
     )
   }),
@@ -60,18 +64,25 @@ export const AzureCognitiveServicesPlugin = define({
       const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
       if (!resourceName) return
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
         if (!item.provider.id.includes("azure-cognitive-services")) continue
         evt.provider.update(item.provider.id, (provider) => {
-          provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
+          provider.settings = {
+            ...provider.settings,
+            baseURL: `https://${resourceName}.cognitiveservices.azure.com/openai`,
+          }
         })
       }
     })
     yield* ctx.aisdk.language(
       Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
-        evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
+        evt.language = selectLanguage(
+          evt.sdk,
+          evt.model.modelID ?? evt.model.id,
+          Boolean(evt.options.useCompletionUrls),
+        )
       }),
     )
   }),

+ 4 - 3
packages/core/src/plugin/provider/cerebras.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { define } from "@opencode-ai/plugin/v2/effect/plugin"
+import { ProviderV2 } from "../../provider"
 
 export const CerebrasPlugin = define({
   id: "opencode.provider.cerebras",
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/cerebras") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
         evt.provider.update(item.provider.id, (provider) => {
-          provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
+          provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
         })
       }
     })

+ 11 - 8
packages/core/src/plugin/provider/cloudflare-workers-ai.ts

@@ -13,10 +13,10 @@ export const CloudflareWorkersAIPlugin = define({
       const item = evt.provider.get(providerID)
       if (!item) return
       evt.provider.update(item.provider.id, (provider) => {
-        if (provider.api.type !== "aisdk") return
-        if (provider.api.url) return
-        const accountId = resolveAccountId(provider.request.body)
-        if (accountId) provider.api.url = workersEndpoint(accountId)
+        if (!ProviderV2.isAISDK(provider.package)) return
+        if (typeof provider.settings?.baseURL === "string") return
+        const accountId = resolveAccountId(provider.settings ?? {})
+        if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
       })
     })
     yield* ctx.aisdk.sdk(
@@ -25,7 +25,7 @@ export const CloudflareWorkersAIPlugin = define({
         if (evt.package !== "@ai-sdk/openai-compatible") return
 
         const accountId = resolveAccountId(evt.options)
-        if (!hasWorkersEndpoint(evt.model.api) && !accountId) return
+        if (!hasWorkersEndpoint(evt.model) && !accountId) return
         const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
         evt.sdk = mod.createOpenAICompatible(
           sdkOptions({
@@ -38,7 +38,7 @@ export const CloudflareWorkersAIPlugin = define({
     yield* ctx.aisdk.language(
       Effect.fn(function* (evt) {
         if (evt.model.providerID !== providerID) return
-        evt.language = evt.sdk.languageModel(evt.model.api.id)
+        evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
       }),
     )
   }),
@@ -52,8 +52,11 @@ function workersEndpoint(accountId: string) {
   return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
 }
 
-function hasWorkersEndpoint(api: ProviderV2.Api) {
-  return api.type === "aisdk" && Boolean(api.url)
+function hasWorkersEndpoint(model: {
+  readonly package?: string
+  readonly settings?: Readonly<Record<string, unknown>>
+}) {
+  return ProviderV2.isAISDK(model.package) && typeof model.settings?.baseURL === "string"
 }
 
 function sdkOptions(options: Record<string, any>) {

+ 3 - 4
packages/core/src/plugin/provider/github-copilot.ts

@@ -34,12 +34,11 @@ export const GithubCopilotPlugin = define({
       Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
         if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
-          evt.language = evt.sdk.languageModel(evt.model.api.id)
+          evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
           return
         }
-        evt.language = shouldUseResponses(evt.model.api.id)
-          ? evt.sdk.responses(evt.model.api.id)
-          : evt.sdk.chat(evt.model.api.id)
+        const id = evt.model.modelID ?? evt.model.id
+        evt.language = shouldUseResponses(id) ? evt.sdk.responses(id) : evt.sdk.chat(id)
       }),
     )
   }),

+ 10 - 12
packages/core/src/plugin/provider/gitlab.ts

@@ -36,26 +36,24 @@ export const GitLabPlugin = define({
         if (evt.model.providerID !== ProviderV2.ID.gitlab) return
         const featureFlags =
           typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
-        if (evt.model.api.id.startsWith("duo-workflow-")) {
+        const id = evt.model.modelID ?? evt.model.id
+        if (id.startsWith("duo-workflow-")) {
           const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie)
           const workflowRef =
-            typeof evt.model.request.body.workflowRef === "string" ? evt.model.request.body.workflowRef : undefined
+            typeof evt.model.settings?.workflowRef === "string" ? evt.model.settings.workflowRef : undefined
           const workflowDefinition =
-            typeof evt.model.request.body.workflowDefinition === "string"
-              ? evt.model.request.body.workflowDefinition
+            typeof evt.model.settings?.workflowDefinition === "string"
+              ? evt.model.settings.workflowDefinition
               : undefined
-          const language = evt.sdk.workflowChat(
-            gitlab.isWorkflowModel(evt.model.api.id) ? evt.model.api.id : "duo-workflow",
-            {
-              featureFlags,
-              workflowDefinition,
-            },
-          )
+          const language = evt.sdk.workflowChat(gitlab.isWorkflowModel(id) ? id : "duo-workflow", {
+            featureFlags,
+            workflowDefinition,
+          })
           if (workflowRef) language.selectedModelRef = workflowRef
           evt.language = language
           return
         }
-        evt.language = evt.sdk.agenticChat(evt.model.api.id, {
+        evt.language = evt.sdk.agenticChat(id, {
           aiGatewayHeaders: evt.options.aiGatewayHeaders,
           featureFlags,
         })

+ 22 - 20
packages/core/src/plugin/provider/google-vertex.ts

@@ -59,25 +59,28 @@ export const GoogleVertexPlugin = define({
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
         if (
-          item.provider.api.package !== "@ai-sdk/google-vertex" &&
+          ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
           !(
             item.provider.id === ProviderV2.ID.googleVertex &&
-            item.provider.api.package.includes("@ai-sdk/openai-compatible")
+            ProviderV2.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible")
           )
         )
           continue
-        const project = resolveProject(item.provider.request.body)
-        const location = String(resolveLocation(item.provider.request.body))
+        const project = resolveProject(item.provider.settings ?? {})
+        const location = String(resolveLocation(item.provider.settings ?? {}))
         evt.provider.update(item.provider.id, (provider) => {
-          if (project) provider.request.body.project = project
-          provider.request.body.location = location
-          if (provider.api.type === "aisdk" && provider.api.url) {
-            provider.api.url = replaceVertexVars(provider.api.url, project, location)
-          }
-          if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) {
-            provider.request.body.fetch = authFetch(provider.request.body.fetch)
+          provider.settings = {
+            ...provider.settings,
+            ...(project ? { project } : {}),
+            location,
+            ...(typeof provider.settings?.baseURL === "string"
+              ? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) }
+              : {}),
+            ...(ProviderV2.packageName(provider.package)?.includes("@ai-sdk/openai-compatible")
+              ? { fetch: authFetch(provider.settings?.fetch) }
+              : {}),
           }
         })
       }
@@ -104,7 +107,7 @@ export const GoogleVertexPlugin = define({
     yield* ctx.aisdk.language(
       Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
-        evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
+        evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
       }),
     )
   }),
@@ -115,21 +118,20 @@ export const GoogleVertexAnthropicPlugin = define({
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue
         const project =
-          item.provider.request.body.project ??
+          item.provider.settings?.project ??
           process.env.GOOGLE_CLOUD_PROJECT ??
           process.env.GCP_PROJECT ??
           process.env.GCLOUD_PROJECT
         const location =
-          item.provider.request.body.location ??
+          item.provider.settings?.location ??
           process.env.GOOGLE_CLOUD_LOCATION ??
           process.env.VERTEX_LOCATION ??
           "global"
         evt.provider.update(item.provider.id, (provider) => {
-          if (project) provider.request.body.project = project
-          provider.request.body.location = location
+          provider.settings = { ...provider.settings, ...(project ? { project } : {}), location }
         })
       }
     })
@@ -162,7 +164,7 @@ export const GoogleVertexAnthropicPlugin = define({
     yield* ctx.aisdk.language(
       Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
-        evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
+        evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
       }),
     )
   }),

+ 5 - 5
packages/core/src/plugin/provider/kilo.ts

@@ -1,17 +1,17 @@
 import { Effect } from "effect"
 import { define } from "@opencode-ai/plugin/v2/effect/plugin"
+import { ProviderV2 } from "../../provider"
 
 export const KiloPlugin = define({
   id: "opencode.provider.kilo",
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
-        if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
+        if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue
         evt.provider.update(item.provider.id, (provider) => {
-          provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
-          provider.request.headers["X-Title"] = "opencode"
+          provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
         })
       }
     })

+ 10 - 6
packages/core/src/plugin/provider/llmgateway.ts

@@ -1,6 +1,7 @@
 import { Effect } from "effect"
 import { define } from "@opencode-ai/plugin/v2/effect/plugin"
 import { Integration } from "../../integration"
+import { ProviderV2 } from "../../provider"
 
 export const LLMGatewayPlugin = define({
   id: "opencode.provider.llmgateway",
@@ -10,14 +11,17 @@ export const LLMGatewayPlugin = define({
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
         if (item.provider.disabled) continue
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
-        if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
+        if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
         if (!configured.has(Integration.ID.make(item.provider.id))) continue
         evt.provider.update(item.provider.id, (provider) => {
-          provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
-          provider.request.headers["X-Title"] = "opencode"
-          provider.request.headers["X-Source"] = "opencode"
+          provider.headers = {
+            ...provider.headers,
+            "HTTP-Referer": "https://opencode.ai/",
+            "X-Title": "opencode",
+            "X-Source": "opencode",
+          }
         })
       }
     })

+ 10 - 6
packages/core/src/plugin/provider/nvidia.ts

@@ -1,18 +1,22 @@
 import { Effect } from "effect"
 import { define } from "@opencode-ai/plugin/v2/effect/plugin"
+import { ProviderV2 } from "../../provider"
 
 export const NvidiaPlugin = define({
   id: "opencode.provider.nvidia",
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
-        if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
+        if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue
         evt.provider.update(item.provider.id, (provider) => {
-          provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
-          provider.request.headers["X-Title"] = "opencode"
-          provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode"
+          provider.headers = {
+            ...provider.headers,
+            "HTTP-Referer": "https://opencode.ai/",
+            "X-Title": "opencode",
+            "X-BILLING-INVOKE-ORIGIN": provider.headers?.["X-BILLING-INVOKE-ORIGIN"] ?? "OpenCode",
+          }
         })
       }
     })

+ 4 - 4
packages/core/src/plugin/provider/openai.ts

@@ -178,8 +178,8 @@ export const OpenAIPlugin = define({
     })
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/openai") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai") continue
         if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
         evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
           // OpenAIPlugin sends OpenAI models through Responses; this alias is a
@@ -194,7 +194,7 @@ export const OpenAIPlugin = define({
         // ChatGPT-plan tokens only authorize codex-eligible models, and the
         // subscription covers usage, so hide the rest and zero the cost.
         evt.model.update(item.provider.id, model.id, (draft) => {
-          if (!OpenAICodex.eligible(draft.api.id)) {
+          if (!OpenAICodex.eligible(draft.modelID ?? draft.id)) {
             draft.enabled = false
             return
           }
@@ -220,7 +220,7 @@ export const OpenAIPlugin = define({
     yield* ctx.aisdk.language(
       Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.openai) return
-        evt.language = evt.sdk.responses(evt.model.api.id)
+        evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
       }),
     )
   }),

+ 21 - 23
packages/core/src/plugin/provider/opencode.ts

@@ -112,45 +112,43 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
         catalog.provider.update(providerID, (provider) => {
           provider.integrationID = Integration.ID.make("opencode")
           if (item.name !== undefined) provider.name = item.name
-          provider.api = item.npm
-            ? { type: "aisdk", package: item.npm, url: item.api }
-            : { type: "native", url: item.api, settings: {} }
-          Object.assign(provider.request.headers, item.options?.headers)
-          Object.assign(provider.request.body, withoutCredentials(item.options))
+          provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
+          provider.settings = {
+            ...provider.settings,
+            ...withoutCredentials(item.options),
+            ...(item.api ? { baseURL: item.api } : {}),
+          }
+          provider.headers = { ...provider.headers, ...item.options?.headers }
         })
 
         for (const [modelID, config] of Object.entries(item.models ?? {})) {
           catalog.model.update(providerID, modelID, (model) => {
             if (config.family !== undefined) model.family = config.family
             if (config.name !== undefined) model.name = config.name
-            if (config.id !== undefined) model.api.id = config.id
+            if (config.id !== undefined) model.modelID = config.id
             if (config.provider !== undefined) {
-              model.api = config.provider.npm
-                ? {
-                    id: model.api.id,
-                    type: "aisdk",
-                    package: config.provider.npm,
-                    url: config.provider.api,
-                  }
-                : { id: model.api.id, type: "native", url: config.provider.api, settings: {} }
+              model.package = config.provider.npm ? ProviderV2.aisdk(config.provider.npm) : undefined
+              if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
             }
             if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
             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
-            const lowerer = ConfigProviderOptionsV1.get(packageName)
-            Object.assign(model.request.headers, config.headers)
-            Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options)))
+            model.headers = { ...model.headers, ...config.headers }
+            model.settings = { ...model.settings, ...ConfigProviderOptionsV1.model(withoutCredentials(config.options)) }
             if (config.variants !== undefined) {
+              model.variants ??= []
               for (const [id, options] of Object.entries(config.variants)) {
                 const variantID = ModelV2.VariantID.make(id)
                 let existing = model.variants.find((item) => item.id === variantID)
                 if (!existing) {
-                  existing = { id: variantID, settings: {}, headers: {}, body: {} }
+                  existing = { id: variantID }
                   model.variants.push(existing)
                 }
-                Object.assign(existing.headers, options.headers)
-                Object.assign(existing.body, lowerer.request(withoutCredentials(options)))
+                existing.headers = { ...existing.headers, ...options.headers }
+                existing.settings = {
+                  ...existing.settings,
+                  ...ConfigProviderOptionsV1.model(withoutCredentials(options)),
+                }
               }
             }
             if (config.release_date !== undefined) {
@@ -169,9 +167,9 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
 
       const item = catalog.provider.get(ProviderV2.ID.opencode)
       if (!item) return
-      const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.request.body.apiKey)
+      const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey)
       catalog.provider.update(item.provider.id, (provider) => {
-        if (!hasKey) provider.request.body.apiKey = "public"
+        if (!hasKey) provider.settings = { ...provider.settings, apiKey: "public" }
       })
       if (hasKey) return
       for (const model of item.models.values()) {

+ 4 - 4
packages/core/src/plugin/provider/openrouter.ts

@@ -1,5 +1,6 @@
 import { Effect } from "effect"
 import { ModelV2 } from "../../model"
+import { ProviderV2 } from "../../provider"
 import { define } from "@opencode-ai/plugin/v2/effect/plugin"
 
 export const OpenRouterPlugin = define({
@@ -7,11 +8,10 @@ export const OpenRouterPlugin = define({
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
         evt.provider.update(item.provider.id, (provider) => {
-          provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
-          provider.request.headers["X-Title"] = "opencode"
+          provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
         })
         for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
           if (!item.models.has(modelID)) continue

+ 1 - 1
packages/core/src/plugin/provider/sap-ai-core.ts

@@ -40,7 +40,7 @@ export const SapAICorePlugin = define({
     yield* ctx.aisdk.language(
       Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
-        evt.language = evt.sdk(evt.model.api.id)
+        evt.language = evt.sdk(evt.model.modelID ?? evt.model.id)
       }),
     )
   }),

+ 4 - 4
packages/core/src/plugin/provider/vercel.ts

@@ -1,16 +1,16 @@
 import { Effect } from "effect"
 import { define } from "@opencode-ai/plugin/v2/effect/plugin"
+import { ProviderV2 } from "../../provider"
 
 export const VercelPlugin = define({
   id: "opencode.provider.vercel",
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/vercel") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/vercel") continue
         evt.provider.update(item.provider.id, (provider) => {
-          provider.request.headers["http-referer"] = "https://opencode.ai/"
-          provider.request.headers["x-title"] = "opencode"
+          provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" }
         })
       }
     })

+ 1 - 1
packages/core/src/plugin/provider/xai.ts

@@ -15,7 +15,7 @@ export const XAIPlugin = define({
     yield* ctx.aisdk.language(
       Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
-        evt.language = evt.sdk.responses(evt.model.api.id)
+        evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
       }),
     )
   }),

+ 9 - 5
packages/core/src/plugin/provider/zenmux.ts

@@ -1,17 +1,21 @@
 import { Effect } from "effect"
 import { define } from "@opencode-ai/plugin/v2/effect/plugin"
+import { ProviderV2 } from "../../provider"
 
 export const ZenmuxPlugin = define({
   id: "opencode.provider.zenmux",
   effect: Effect.fn(function* (ctx) {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
-        if (item.provider.api.type !== "aisdk") continue
-        if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
-        if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue
+        if (!ProviderV2.isAISDK(item.provider.package)) continue
+        if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
+        if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
         evt.provider.update(item.provider.id, (provider) => {
-          provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/"
-          provider.request.headers["X-Title"] ??= "opencode"
+          provider.headers = {
+            "HTTP-Referer": "https://opencode.ai/",
+            "X-Title": "opencode",
+            ...provider.headers,
+          }
         })
       }
     })

+ 15 - 11
packages/core/src/plugin/variant.ts

@@ -1,8 +1,9 @@
 export * as VariantPlugin from "./variant"
 
-import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
 import { Effect } from "effect"
 import { define } from "@opencode-ai/plugin/v2/effect/plugin"
+import { ModelV2 } from "../model"
+import { ProviderV2 } from "../provider"
 
 export const Plugin = define({
   id: "opencode.variant",
@@ -11,14 +12,15 @@ export const Plugin = define({
       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)
+            const generated = generate(draft, record.provider)
             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))
+            const variants = draft.variants ?? []
+            const explicit = new Map(variants.map((variant) => [variant.id, variant]))
+            const generatedIDs = new Set<string>(generated.map((variant) => variant.id))
             draft.variants = [
               ...generated.map((variant) => explicit.get(variant.id) ?? variant),
-              ...draft.variants.filter((variant) => !generatedIDs.has(variant.id)),
+              ...variants.filter((variant) => !generatedIDs.has(variant.id)),
             ]
           })
         }
@@ -27,14 +29,16 @@ export const Plugin = define({
   }),
 })
 
-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()
+export function generate(
+  model: { readonly id: string; readonly modelID?: string; readonly package?: string },
+  provider?: { readonly package: string },
+): NonNullable<ModelV2.Info["variants"]> {
+  const packageName = model.package ?? provider?.package
+  if (!ProviderV2.isAISDK(packageName) || ProviderV2.packageName(packageName) !== "@ai-sdk/openai-compatible") return []
+  const ids = `${model.id} ${model.modelID ?? ""}`.toLowerCase()
   if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
   return ["high", "max"].map((id) => ({
-    id,
+    id: ModelV2.VariantID.make(id),
     settings: { reasoningEffort: id },
-    headers: {},
-    body: {},
   }))
 }

+ 114 - 13
packages/core/src/provider.ts

@@ -1,20 +1,106 @@
 export * as ProviderV2 from "./provider"
 
-import { Types } from "effect"
+import { Effect, Schema } from "effect"
+import { pathToFileURL } from "url"
 import { Provider } from "@opencode-ai/schema/provider"
+import type { ProviderPackageDefinition } from "@opencode-ai/llm"
+import { Npm } from "./npm"
+import type { DeepMutable } from "./schema"
 
 export const ID = Provider.ID
 export type ID = typeof ID.Type
 
-export const AISDK = Provider.AISDK
+export const AISDK_PREFIX = "aisdk:"
+export const isAISDK = (value: string | undefined) => value?.startsWith(AISDK_PREFIX) ?? false
+export const aisdk = (value: string) => (isAISDK(value) ? value : `${AISDK_PREFIX}${value}`)
+export const packageName = (value: string | undefined) => {
+  if (value === undefined || !isAISDK(value)) return value
+  return value.slice(AISDK_PREFIX.length)
+}
+
+type Json = Schema.Schema.Type<typeof Schema.Json>
+const JsonRecord = Schema.Record(Schema.String, Schema.Json)
+const decodeJsonRecord = Schema.decodeUnknownSync(JsonRecord)
+
+export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderV2.LoadError", {
+  package: Schema.String,
+  cause: Schema.Defect(),
+}) {}
+export type ProviderPackage = ProviderPackageDefinition
+
+const packages = new Map<string, Promise<unknown>>()
 
-export const Native = Provider.Native
+export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) {
+  const resolved = yield* Effect.sync(() => {
+    if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/llm/")) return specifier
+    try {
+      return import.meta.resolve(specifier)
+    } catch {
+      return undefined
+    }
+  })
+  if (resolved) return yield* importPackage(specifier, resolved)
+  if (!npm) {
+    return yield* new LoadError({
+      package: specifier,
+      cause: new Error(`Provider package ${specifier} is not installed`),
+    })
+  }
+  const parts = specifier.split("/")
+  const root = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? specifier)
+  const installed = yield* npm.add(root).pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause })))
+  const entrypoint = yield* Effect.try({
+    try: () => import.meta.resolve(specifier, pathToFileURL(`${installed.directory}/`).href),
+    catch: (cause) => new LoadError({ package: specifier, cause }),
+  })
+  return yield* importPackage(specifier, entrypoint)
+})
 
-export const Api = Provider.Api
-export type Api = Provider.Api
-export type MutableApi<T extends Api = Api> = T extends Api
-  ? Omit<Types.DeepMutable<T>, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any })
-  : never
+export function mergeOverlay(
+  base: Readonly<Record<string, unknown>> | undefined,
+  overlay: Readonly<Record<string, unknown>> | undefined,
+): Record<string, Json> | undefined {
+  if (base === undefined) return overlay && decodeJsonRecord({ ...overlay })
+  if (overlay === undefined) return decodeJsonRecord({ ...base })
+  return decodeJsonRecord(
+    Object.fromEntries(
+      new Set([...Object.keys(base), ...Object.keys(overlay)]).values().map((key): [string, unknown] => {
+        const left = base[key]
+        const right = overlay[key]
+        if (right === undefined) return [key, left]
+        if (
+          typeof left === "object" &&
+          left !== null &&
+          !Array.isArray(left) &&
+          typeof right === "object" &&
+          right !== null &&
+          !Array.isArray(right)
+        )
+          return [
+            key,
+            mergeOverlay(left as Readonly<Record<string, unknown>>, right as Readonly<Record<string, unknown>>) ?? {},
+          ]
+        return [key, right]
+      }),
+    ),
+  )
+}
+
+export function mergeHeaders(
+  base: Readonly<Record<string, string>> | undefined,
+  overlay: Readonly<Record<string, string>> | undefined,
+) {
+  if (base === undefined) return overlay && { ...overlay }
+  if (overlay === undefined) return { ...base }
+  return Object.fromEntries(
+    [...Object.entries(base), ...Object.entries(overlay)]
+      .reduce((result, entry) => {
+        result.set(entry[0].toLowerCase(), entry)
+        return result
+      }, new Map<string, [string, string]>())
+      .values(),
+  )
+}
 
 export const Request = Provider.Request
 export type Request = Provider.Request
@@ -25,9 +111,24 @@ export type Settings = Provider.Settings
 export const Info = Provider.Info
 export type Info = Provider.Info
 
-export type MutableRequest = Types.DeepMutable<Request>
+export type MutableInfo = DeepMutable<Info>
 
-export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request"> & {
-  api: MutableApi
-  request: MutableRequest
-}
+const importPackage = Effect.fn("ProviderV2.importPackage")(function* (specifier: string, entrypoint: string) {
+  const module = yield* Effect.tryPromise({
+    try: () => {
+      const existing = packages.get(entrypoint)
+      if (existing) return existing
+      const loaded = import(entrypoint)
+      packages.set(entrypoint, loaded)
+      return loaded
+    },
+    catch: (cause) => new LoadError({ package: specifier, cause }),
+  })
+  if (typeof module !== "object" || module === null || typeof (module as { model?: unknown }).model !== "function") {
+    return yield* new LoadError({
+      package: specifier,
+      cause: new Error(`Provider package ${specifier} does not export model(modelID, settings)`),
+    })
+  }
+  return module as ProviderPackageDefinition
+})

+ 4 - 1
packages/core/src/session/runner/llm.ts

@@ -213,7 +213,10 @@ const layer = Layer.effect(
         ]
           .filter((part): part is string => part !== undefined && part.length > 0)
           .map(SystemPart.make),
-        messages: [...toLLMMessages(context, model), ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : [])],
+        messages: [
+          ...toLLMMessages(context, resolved.ref),
+          ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : []),
+        ],
         tools: toolMaterialization?.definitions ?? [],
         toolChoice: isLastStep ? "none" : undefined,
       })

+ 146 - 66
packages/core/src/session/runner/model.ts

@@ -1,7 +1,7 @@
 export * as SessionRunnerModel from "./model"
 
 import { makeLocationNode } from "../../effect/app-node"
-import { type Model } from "@opencode-ai/llm"
+import { Model } from "@opencode-ai/llm"
 // ast-grep-ignore: no-star-import
 import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
 // ast-grep-ignore: no-star-import
@@ -11,10 +11,12 @@ import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses"
 import { Auth, type AnyRoute } from "@opencode-ai/llm/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 "../../npm"
 import { OpenAICodex } from "../../plugin/provider/openai-codex"
 import { ProviderV2 } from "../../provider"
 import { SessionSchema } from "../schema"
@@ -55,16 +57,16 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
   }
 }
 
-export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiError>()(
-  "SessionRunnerModel.UnsupportedApiError",
+export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
+  "SessionRunnerModel.UnsupportedPackageError",
   {
     providerID: ProviderV2.ID,
     modelID: ModelV2.ID,
-    api: Schema.String,
+    package: Schema.String,
   },
 ) {
   override get message() {
-    return `Unsupported API for ${this.providerID}/${this.modelID}: ${this.api}`
+    return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}`
   }
 }
 
@@ -72,7 +74,7 @@ export type Error =
   | ModelNotSelectedError
   | ModelUnavailableError
   | VariantUnavailableError
-  | UnsupportedApiError
+  | UnsupportedPackageError
   | Integration.AuthorizationError
 
 export interface Resolved {
@@ -104,41 +106,50 @@ export const resolved = (model: Model, variant?: ModelV2.VariantID): Resolved =>
 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.request.body.apiKey ?? model.api.settings?.apiKey
+  const value = model.settings?.apiKey
   if (typeof value === "string") return Auth.value(value)
 }
 
-const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
-  const body = model.request.body
-  const httpBody = Object.hasOwn(body, "apiKey")
-    ? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
-    : body
-  return route.with({
+const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
+  route.with({
     provider: model.providerID,
-    endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
-    headers: model.request.headers,
+    endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
+    headers: providerHeaders(model),
     providerOptions: providerOptions(model),
-    http: { body: httpBody },
+    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 (Object.keys(model.request.settings).length === 0) return undefined
-  if (model.api.type !== "aisdk") return undefined
-  if (model.api.package === "@ai-sdk/openai") return { openai: model.request.settings }
-  if (model.api.package === "@ai-sdk/anthropic") return { anthropic: model.request.settings }
-  if (model.api.package === "@ai-sdk/openai-compatible") return { openai: model.request.settings }
+  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" || variantID === undefined ? model.request.variant : variantID
-  const variant = model.variants.find((item) => item.id === id)
+  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({
@@ -150,81 +161,140 @@ export const withVariant = (
   return Effect.succeed(
     variant
       ? produce(model, (draft) => {
-          Object.assign(draft.request.settings, variant.settings)
-          Object.assign(draft.request.headers, variant.headers)
-          Object.assign(draft.request.body, variant.body)
+          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,
   )
 }
 
-const apiName = (model: ModelV2.Info) =>
-  model.api.type === "aisdk" ? `${model.api.type}:${model.api.package}` : model.api.type
+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,
-): Effect.Effect<Model, UnsupportedApiError> => {
+  dependencies: Dependencies = {},
+): Effect.Effect<Model, UnsupportedPackageError> => {
   const resolved =
     credential?.type !== "key" || credential.metadata === undefined
       ? model
       : produce(model, (draft) => {
-          Object.assign(draft.request.body, credential.metadata)
+          draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata)
         })
+  const packageName = ProviderV2.packageName(resolved.package)
   const key = apiKey(resolved, credential)
-  if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
-    // ChatGPT-plan OAuth tokens are not API-key credentials: the public API rejects
-    // them, so requests must target the codex backend with the account header.
-    if (OpenAICodex.isChatGPT(credential)) {
-      const account = OpenAICodex.accountID(credential)
-      return Effect.succeed(
-        withDefaults(resolved, 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: resolved.api.id }),
-      )
-    }
+
+  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.api.id }),
+        .model({ id: resolved.modelID ?? resolved.id }),
     )
   }
-  if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/anthropic") {
+  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.api.id }),
+        .model({ id: resolved.modelID ?? resolved.id }),
     )
   }
-  if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai-compatible" && resolved.api.url) {
+  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.api.id }),
+        .model({ id: resolved.modelID ?? resolved.id }),
     )
   }
-  return Effect.fail(
-    new UnsupportedApiError({
-      providerID: resolved.providerID,
-      modelID: resolved.id,
-      api: apiName(resolved),
-    }),
-  )
+  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 settings = {
+      ...resolved.settings,
+      ...(credential?.type === "key" ? { apiKey: credential.key } : {}),
+      ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
+      ...credential?.metadata,
+      headers: resolved.headers,
+      body: resolved.body,
+      limits: { context: resolved.limit.context, output: resolved.limit.output },
+    }
+    return yield* Effect.try({
+      try: () =>
+        Model.update(module.model(resolved.modelID ?? resolved.id, settings), { provider: resolved.providerID }),
+      catch: () => unsupported(resolved),
+    })
+  })
 }
 
-export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, credential?: Credential.Value) =>
-  withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
+const isNativeOpenAI = (packageName: string | undefined) =>
+  packageName === "@opencode-ai/llm/providers/openai" ||
+  packageName?.startsWith("@opencode-ai/llm/providers/openai/") === true
 
-export const supported = (model: ModelV2.Info) =>
-  model.api.type === "aisdk" &&
-  (model.api.package === "@ai-sdk/openai" ||
-    model.api.package === "@ai-sdk/anthropic" ||
-    (model.api.package === "@ai-sdk/openai-compatible" && model.api.url !== undefined))
+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 })
+}
+
+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(
@@ -232,6 +302,8 @@ const layer = Layer.effect(
   Effect.gen(function* () {
     const catalog = yield* Catalog.Service
     const integrations = yield* Integration.Service
+    const npm = yield* Npm.Service
+    const aisdk = yield* AISDK.Service
     return Service.of({
       resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
         // Location plugins populate and filter the catalog asynchronously during layer startup.
@@ -257,6 +329,10 @@ const layer = Layer.effect(
           session,
           selected,
           connection ? yield* integrations.connection.resolve(connection) : undefined,
+          {
+            loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
+            loadAISDK: (model) => aisdk.model(model),
+          },
         )
         return {
           model,
@@ -271,4 +347,8 @@ const layer = Layer.effect(
   }),
 )
 
-export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, Integration.node] })
+export const node = makeLocationNode({
+  service: Service,
+  layer,
+  deps: [Catalog.node, Integration.node, Npm.node, AISDK.node],
+})

+ 5 - 5
packages/core/src/session/runner/to-llm-message.ts

@@ -4,10 +4,10 @@ import {
   ToolOutput,
   ToolResultPart,
   type ContentPart,
-  type Model,
   type ProviderMetadata,
 } from "@opencode-ai/llm"
 import { Option, Schema } from "effect"
+import type { ModelV2 } from "../../model"
 import { SessionMessage } from "../message"
 import type { FileAttachment } from "@opencode-ai/schema/prompt"
 
@@ -88,9 +88,9 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
   }
 }
 
-const assistant = (message: SessionMessage.Assistant, model: Model) => {
+const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => {
   const sameModel =
-    String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id)
+    String(message.model.providerID) === String(model.providerID) && String(message.model.id) === String(model.id)
   const reuseProviderMetadata = sameModel && message.error === undefined
   const content = message.content.flatMap((item): ContentPart[] => {
     if (item.type === "text") return [{ type: "text", text: item.text }]
@@ -133,7 +133,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model) => {
   ]
 }
 
-function toLLMMessage(message: SessionMessage.Message, model: Model): Message[] {
+function toLLMMessage(message: SessionMessage.Message, model: ModelV2.Ref): Message[] {
   switch (message.type) {
     case "agent-switched":
     case "model-switched":
@@ -197,5 +197,5 @@ ${message.recent}
 }
 
 /** Translate projected V2 Session history into canonical @opencode-ai/llm context. */
-export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: Model) =>
+export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: ModelV2.Ref) =>
   messages.flatMap((message) => toLLMMessage(message, model))

+ 44 - 37
packages/core/src/v1/config/migrate.ts

@@ -2,10 +2,12 @@ export * as ConfigMigrateV1 from "./migrate"
 
 import { ConfigV1 } from "./config"
 import { ConfigAgentV1 } from "./agent"
+import { ConfigCommandV1 } from "./command"
 import { ConfigMCPV1 } from "./mcp"
 import { ConfigPermissionV1 } from "./permission"
 import { ConfigProviderV1 } from "./provider"
 import { ConfigProviderOptionsV1 } from "./provider-options"
+import { ProviderV2 } from "../../provider"
 
 const keys = new Set([
   "logLevel",
@@ -48,7 +50,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) {
   return {
     $schema: info.$schema,
     shell: info.shell,
-    model: info.model,
+    model: modelSelection(info.model),
     default_agent: info.default_agent,
     autoupdate: info.autoupdate,
     share: info.share ?? (info.autoshare ? "auto" : undefined),
@@ -72,7 +74,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) {
       buffer: info.compaction.reserved,
     },
     skills: info.skills && [...(info.skills.paths ?? []), ...(info.skills.urls ?? [])],
-    commands: info.command,
+    commands: commands(info.command),
     instructions: info.instructions,
     references: info.references ?? info.reference,
     plugins: info.plugin?.map((plugin) =>
@@ -126,8 +128,7 @@ export function migrateAgent(info: ConfigAgentV1.Info) {
     ...(info.top_p === undefined ? {} : { top_p: info.top_p }),
   }
   return {
-    model: info.model,
-    variant: info.variant,
+    model: modelSelection(info.model, info.variant),
     request: Object.keys(body).length ? { body } : undefined,
     system: info.prompt,
     description: info.description,
@@ -140,6 +141,32 @@ export function migrateAgent(info: ConfigAgentV1.Info) {
   }
 }
 
+function commands(info?: Readonly<Record<string, ConfigCommandV1.Info>>) {
+  if (!info) return undefined
+  return Object.fromEntries(
+    Object.entries(info).map(([id, command]) => [
+      id,
+      {
+        template: command.template,
+        description: command.description,
+        agent: command.agent,
+        model: modelSelection(command.model, command.variant),
+        subtask: command.subtask,
+      },
+    ]),
+  )
+}
+
+function modelSelection(input?: string, variant?: string) {
+  if (input === undefined || !/^[^/#]+\/[^#]+$/.test(input)) return undefined
+  const separator = input.indexOf("/")
+  return {
+    providerID: input.slice(0, separator),
+    model: input.slice(separator + 1),
+    ...(variant === undefined || variant.length === 0 || variant.includes("#") ? {} : { variant }),
+  }
+}
+
 function mcp(info: typeof ConfigV1.Info.Type) {
   const servers = Object.fromEntries(
     Object.entries(info.mcp ?? {}).flatMap(([name, server]) =>
@@ -184,31 +211,22 @@ function providers(info?: Readonly<Record<string, ConfigProviderV1.Info>>) {
 }
 
 function migrateProvider(info: ConfigProviderV1.Info) {
-  const lowerer = ConfigProviderOptionsV1.get(info.npm)
-  const options = lowerer.provider(info.options ?? {})
-  const url = info.api ?? options.url
+  const options = ConfigProviderOptionsV1.provider(info.options ?? {})
   return {
     name: info.name,
     env: info.env,
-    api: info.npm
-      ? {
-          type: "aisdk" as const,
-          package: info.npm,
-          ...(url === undefined ? {} : { url }),
-          settings: options.settings ?? {},
-        }
-      : undefined,
-    request: info.options && { headers: options.headers, body: options.body },
+    package: info.npm ? ProviderV2.aisdk(info.npm) : undefined,
+    settings: info.api ? { ...options.settings, baseURL: info.api } : options.settings,
+    headers: info.options && options.headers,
+    body: info.options && options.body,
     models:
       info.models &&
-      Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model, info.npm)])),
+      Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model)])),
   }
 }
 
-function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) {
-  const packageID = info.provider?.npm ?? packageName
-  const lowerer = ConfigProviderOptionsV1.get(packageID)
-  const request = info.options && lowerer.request(info.options)
+function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
+  const settings = info.options && ConfigProviderOptionsV1.model(info.options)
   const costs = info.cost && [
     {
       input: info.cost.input,
@@ -231,29 +249,18 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
       ? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
       : undefined
   return {
+    modelID: info.id,
     family: info.family,
     name: info.name,
-    api: info.provider?.npm
-      ? {
-          ...(info.id === undefined ? {} : { id: info.id }),
-          type: "aisdk" as const,
-          package: info.provider.npm,
-          ...(info.provider.api === undefined ? {} : { url: info.provider.api }),
-          settings: {},
-        }
-      : info.id === undefined
-        ? undefined
-        : { id: info.id },
+    package: info.provider?.npm ? ProviderV2.aisdk(info.provider.npm) : undefined,
+    settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings,
     capabilities,
-    request: (info.headers || request) && {
-      headers: info.headers,
-      body: request,
-    },
+    headers: info.headers,
     variants:
       info.variants &&
       Object.entries(info.variants).map(([id, options]) => ({
         id,
-        body: lowerer.request(options),
+        settings: ConfigProviderOptionsV1.model(options),
       })),
     cost: costs,
     disabled: info.status === "deprecated" ? true : undefined,

+ 16 - 212
packages/core/src/v1/config/provider-options.ts

@@ -3,225 +3,29 @@ export * as ConfigProviderOptionsV1 from "./provider-options"
 type Options = Readonly<Record<string, unknown>>
 
 export interface ProviderResult {
+  readonly settings: Record<string, unknown>
   readonly headers?: Record<string, string>
   readonly body?: Record<string, unknown>
-  readonly url?: string
-  readonly settings?: Record<string, unknown>
 }
 
-export interface Lowerer {
-  readonly provider: (options: Options) => ProviderResult
-  readonly request: (options: Options) => Record<string, unknown>
-}
-
-export function get(packageName?: string): Lowerer {
-  const key = packageName ?? ""
-  return Object.hasOwn(lowerers, key) ? lowerers[key]! : raw
-}
-
-const raw: Lowerer = {
-  provider(options) {
-    return { body: clone(options) }
-  },
-  request: clone,
-}
-
-const openai: Lowerer = {
-  provider(options) {
-    return {
-      url: string(options.baseURL),
-      headers: compact({
-        Authorization: bearer(options.apiKey),
-        "OpenAI-Organization": string(options.organization),
-        "OpenAI-Project": string(options.project),
-        ...headers(options.headers),
-      }),
-      body: body(options.body),
-      settings: omit(options, ["apiKey", "baseURL", "organization", "project", "headers", "body"]),
-    }
-  },
-  request(options) {
-    const result = snake(options)
-    if (options.reasoningEffort !== undefined || options.reasoningSummary !== undefined) {
-      result.reasoning = {
-        ...(isRecord(result.reasoning) ? result.reasoning : {}),
-        ...(options.reasoningEffort !== undefined ? { effort: options.reasoningEffort } : {}),
-        ...(options.reasoningSummary !== undefined ? { summary: options.reasoningSummary } : {}),
-      }
-      delete result.reasoning_effort
-      delete result.reasoning_summary
-    }
-    if (options.textVerbosity !== undefined) {
-      result.text = { ...(isRecord(result.text) ? result.text : {}), verbosity: options.textVerbosity }
-      delete result.text_verbosity
-    }
-    return result
-  },
-}
-
-const anthropic: Lowerer = {
-  provider(options) {
-    return {
-      url: string(options.baseURL),
-      headers: compact({
-        "x-api-key": string(options.apiKey),
-        Authorization: options.authToken ? bearer(options.authToken) : undefined,
-        ...headers(options.headers),
-      }),
-      body: body(options.body),
-      settings: omit(options, ["apiKey", "authToken", "baseURL", "headers", "body"]),
-    }
-  },
-  request(options) {
-    const result = snake(options)
-    if (options.effort !== undefined || options.taskBudget !== undefined) {
-      result.output_config = compactUnknown({ effort: options.effort, task_budget: options.taskBudget })
-      delete result.effort
-      delete result.task_budget
-    }
-    if (isRecord(options.metadata) && options.metadata.userId !== undefined) {
-      result.metadata = { ...(isRecord(result.metadata) ? result.metadata : {}), user_id: options.metadata.userId }
-    }
-    return result
-  },
-}
-
-const google: Lowerer = {
-  provider(options) {
-    return {
-      url: string(options.baseURL),
-      headers: compact({ "x-goog-api-key": string(options.apiKey), ...headers(options.headers) }),
-      body: body(options.body),
-      settings: omit(options, ["apiKey", "baseURL", "headers", "body"]),
-    }
-  },
-  request(options) {
-    const generationConfig = pick(options, ["thinkingConfig", "responseModalities", "mediaResolution", "imageConfig"])
-    return {
-      ...omit(options, ["thinkingConfig", "responseModalities", "mediaResolution", "imageConfig"]),
-      ...(Object.keys(generationConfig).length ? { generationConfig } : {}),
-    }
-  },
-}
-
-const azure: Lowerer = {
-  provider(options) {
-    return {
-      url: string(options.baseURL),
-      headers: compact({ "api-key": string(options.apiKey), ...headers(options.headers) }),
-      body: body(options.body),
-      settings: omit(options, ["apiKey", "baseURL", "headers", "body"]),
-    }
-  },
-  request: openai.request,
-}
-
-const bedrock: Lowerer = {
-  provider(options) {
-    return direct(options)
-  },
-  request(options) {
-    return { additionalModelRequestFields: clone(options) }
-  },
-}
-
-const openaiCompatible: Lowerer = {
-  provider(options) {
-    return { ...direct(options, ["baseURL"]), url: string(options.baseURL) }
-  },
-  request(options) {
-    const result = clone(options)
-    if (options.reasoningEffort !== undefined) {
-      result.reasoning_effort = options.reasoningEffort
-      delete result.reasoningEffort
-    }
-    return result
-  },
-}
-
-const lowerers: Readonly<Record<string, Lowerer>> = {
-  "@ai-sdk/openai": openai,
-  "@ai-sdk/anthropic": anthropic,
-  "@ai-sdk/google-vertex/anthropic": anthropic,
-  "@ai-sdk/google": google,
-  "@ai-sdk/google-vertex": google,
-  "@ai-sdk/azure": azure,
-  "@ai-sdk/amazon-bedrock": bedrock,
-  "@ai-sdk/openai-compatible": openaiCompatible,
-  "@ai-sdk/cerebras": openaiCompatible,
-  "@ai-sdk/deepinfra": openaiCompatible,
-  "@ai-sdk/groq": openaiCompatible,
-  "@ai-sdk/mistral": openaiCompatible,
-  "@ai-sdk/togetherai": openaiCompatible,
-  "@ai-sdk/xai": openaiCompatible,
-  "@openrouter/ai-sdk-provider": openaiCompatible,
-  "ai-gateway-provider": openaiCompatible,
-  "venice-ai-sdk-provider": openaiCompatible,
-}
-
-function direct(options: Options, extraKeys: ReadonlyArray<string> = []): ProviderResult {
+export function provider(options: Options): ProviderResult {
+  const headers = options.headers
+  const body = options.body
+  const settings = Object.fromEntries(Object.entries(options).filter(([key]) => key !== "headers" && key !== "body"))
+  const headerOverlay =
+    typeof headers === "object" && headers !== null && !Array.isArray(headers)
+      ? Object.fromEntries(
+          Object.entries(headers).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
+        )
+      : undefined
+  const bodyOverlay = typeof body === "object" && body !== null && !Array.isArray(body) ? { ...body } : undefined
   return {
-    headers: headers(options.headers),
-    body: body(options.body),
-    settings: omit(options, ["headers", "body", ...extraKeys]),
+    settings,
+    headers: headerOverlay,
+    body: bodyOverlay,
   }
 }
 
-function body(input: unknown) {
-  if (!isRecord(input)) return undefined
-  return { ...input }
-}
-
-function snake(options: Options) {
-  return Object.fromEntries(Object.entries(options).map(([key, value]) => [snakeKey(key), snakeValue(value)]))
-}
-
-function snakeValue(value: unknown): unknown {
-  if (Array.isArray(value)) return value.map(snakeValue)
-  if (!isRecord(value)) return value
-  return Object.fromEntries(Object.entries(value).map(([key, value]) => [snakeKey(key), snakeValue(value)]))
-}
-
-function snakeKey(key: string) {
-  return key.replace(/[A-Z]/g, (match) => "_" + match.toLowerCase())
-}
-
-function clone(options: Options) {
+export function model(options: Options) {
   return { ...options }
 }
-
-function omit(options: Options, keys: ReadonlyArray<string>) {
-  return Object.fromEntries(Object.entries(options).filter(([key]) => !keys.includes(key)))
-}
-
-function pick(options: Options, keys: ReadonlyArray<string>) {
-  return Object.fromEntries(Object.entries(options).filter(([key]) => keys.includes(key)))
-}
-
-function headers(input: unknown) {
-  if (!isRecord(input)) return undefined
-  return Object.fromEntries(
-    Object.entries(input).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
-  )
-}
-
-function compact(input: Record<string, string | undefined>) {
-  const entries = Object.entries(input).filter((entry): entry is [string, string] => entry[1] !== undefined)
-  return entries.length ? Object.fromEntries(entries) : undefined
-}
-
-function compactUnknown(input: Record<string, unknown>) {
-  return Object.fromEntries(Object.entries(input).filter((entry) => entry[1] !== undefined))
-}
-
-function string(input: unknown) {
-  return typeof input === "string" && input ? input : undefined
-}
-
-function bearer(input: unknown) {
-  return typeof input === "string" && input ? `Bearer ${input}` : undefined
-}
-
-function isRecord(input: unknown): input is Record<string, unknown> {
-  return typeof input === "object" && input !== null && !Array.isArray(input)
-}

+ 85 - 0
packages/core/test/aisdk.test.ts

@@ -0,0 +1,85 @@
+import type { LanguageModelV3CallOptions } from "@ai-sdk/provider"
+import { AISDK } from "@opencode-ai/core/aisdk"
+import { ModelV2 } from "@opencode-ai/core/model"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { LLM } from "@opencode-ai/llm"
+import { LLMClient } from "@opencode-ai/llm/route"
+import { expect } from "bun:test"
+import { Effect } from "effect"
+import { testEffect } from "./lib/effect"
+
+const it = testEffect(AISDK.locationLayer)
+type Fetch = (input: Parameters<typeof fetch>[0], init?: RequestInit) => Promise<Response>
+
+const model = (packageName: string, settings: Record<string, unknown> = {}) =>
+  ModelV2.Info.make({
+    ...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")),
+    modelID: ModelV2.ID.make("api-model"),
+    package: ProviderV2.aisdk(packageName),
+    settings,
+    limit: { context: 100, output: 20 },
+  })
+
+it.effect("keys language models by package and flattened overlays", () =>
+  Effect.gen(function* () {
+    const aisdk = yield* AISDK.Service
+    const loaded: string[] = []
+    yield* aisdk.hook.sdk((event) => {
+      loaded.push(event.package)
+      event.sdk = { languageModel: () => ({ package: event.package }) }
+    })
+
+    const first = yield* aisdk.language(model("first", { region: "us-east-1" }))
+    const second = yield* aisdk.language(model("second", { region: "us-east-1" }))
+    const third = yield* aisdk.language(
+      model("second", { region: "us-east-1", fetch: async () => new Response("ok") }),
+    )
+
+    expect(first).not.toBe(second)
+    expect(second).not.toBe(third)
+    expect(loaded).toEqual(["first", "second", "second"])
+  }),
+)
+
+it.effect("projects request settings, headers, and raw body overlays", () =>
+  Effect.gen(function* () {
+    const aisdk = yield* AISDK.Service
+    let wrappedFetch: Fetch | undefined
+    let body: unknown
+    const customFetch: Fetch = async (_input, init) => {
+      body = init?.body
+      return new Response("ok")
+    }
+    yield* aisdk.hook.sdk((event) => {
+      wrappedFetch = event.options.fetch
+      event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
+    })
+
+    const resolved = yield* aisdk.model(
+      ModelV2.Info.make({
+        ...model("@ai-sdk/google", {
+          apiKey: "secret",
+          fetch: customFetch,
+          thinkingConfig: { thinkingBudget: 1024 },
+        }),
+        headers: { "x-test": "header" },
+        body: { safety_setting: "strict" },
+      }),
+    )
+    const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
+      LLM.request({ model: resolved, prompt: "Hello" }),
+    )
+
+    expect(prepared.body.providerOptions).toEqual({
+      google: { thinkingConfig: { thinkingBudget: 1024 } },
+    })
+    expect(prepared.body.headers).toEqual({ "x-test": "header" })
+    expect(wrappedFetch).toBeFunction()
+    if (wrappedFetch === undefined) return yield* Effect.die("Expected wrapped fetch")
+    const fetchRequest = wrappedFetch
+    yield* Effect.promise(() =>
+      fetchRequest("https://provider.example", { method: "POST", body: JSON.stringify({ model: "api-model" }) }),
+    )
+    expect(JSON.parse(String(body))).toEqual({ model: "api-model", safety_setting: "strict" })
+  }),
+)

+ 34 - 54
packages/core/test/catalog.test.ts

@@ -61,14 +61,14 @@ describe("CatalogV2", () => {
       })
 
       expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined()
       yield* credentials.create({
         integrationID,
         label: "Second",
         value: Credential.Key.make({ type: "key", key: "second", metadata: { tenant: "two" } }),
       })
       expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined()
     }).pipe(Effect.provide(localCatalogLayer))
   })
 
@@ -130,111 +130,91 @@ describe("CatalogV2", () => {
     ),
   )
 
-  it.effect("normalizes provider baseURL into api url", () =>
+  it.effect("stores provider package settings", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       const providerID = ProviderV2.ID.make("test")
       yield* catalog.transform((catalog) =>
         catalog.provider.update(providerID, (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://default.example.com",
-          }
-          provider.request.body.baseURL = "https://override.example.com"
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://override.example.com" }
         }),
       )
 
-      expect(required(yield* catalog.provider.get(providerID)).api).toEqual({
-        type: "aisdk",
-        package: "@ai-sdk/openai-compatible",
-        url: "https://override.example.com",
+      expect(required(yield* catalog.provider.get(providerID))).toMatchObject({
+        package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
+        settings: { baseURL: "https://override.example.com" },
       })
     }),
   )
 
-  it.effect("normalizes model baseURL into api url", () =>
+  it.effect("uses model package settings over provider settings", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       const providerID = ProviderV2.ID.make("test")
       const modelID = ModelV2.ID.make("model")
       yield* catalog.transform((catalog) => {
         catalog.provider.update(providerID, (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://provider.example.com",
-          }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://provider.example.com" }
         })
         catalog.model.update(providerID, modelID, (model) => {
-          model.api = {
-            id: modelID,
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://model.example.com",
-          }
-          model.request.body.baseURL = "https://override.example.com"
+          model.modelID = ModelV2.ID.make("upstream-model")
+          model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          model.settings = { baseURL: "https://override.example.com" }
         })
       })
 
-      expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
-        id: modelID,
-        type: "aisdk",
-        package: "@ai-sdk/openai-compatible",
-        url: "https://override.example.com",
-        settings: {},
+      expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({
+        modelID: ModelV2.ID.make("upstream-model"),
+        package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
+        settings: { baseURL: "https://override.example.com" },
       })
     }),
   )
 
-  it.effect("resolves default model api from provider api", () =>
+  it.effect("resolves default model package settings from the provider", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       const providerID = ProviderV2.ID.make("test")
       const modelID = ModelV2.ID.make("model")
       yield* catalog.transform((catalog) => {
         catalog.provider.update(providerID, (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://provider.example.com",
-          }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://provider.example.com" }
         })
         catalog.model.update(providerID, modelID, () => {})
       })
 
-      expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
-        id: modelID,
-        type: "aisdk",
-        package: "@ai-sdk/openai-compatible",
-        url: "https://provider.example.com",
+      expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({
+        package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
+        settings: { baseURL: "https://provider.example.com" },
       })
     }),
   )
 
-  it.effect("resolves provider and model request merges", () =>
+  it.effect("resolves provider and model overlay merges", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       const providerID = ProviderV2.ID.make("test")
       const modelID = ModelV2.ID.make("model")
       yield* catalog.transform((catalog) => {
         catalog.provider.update(providerID, (provider) => {
-          provider.request.headers.provider = "provider"
-          provider.request.headers.shared = "provider"
-          provider.request.body.provider = true
+          provider.settings = { provider: true, shared: "provider" }
+          provider.headers = { provider: "provider", shared: "provider" }
+          provider.body = { provider: true, shared: "provider" }
         })
         catalog.model.update(providerID, modelID, (model) => {
-          model.request.headers.model = "model"
-          model.request.headers.shared = "model"
-          model.request.body.model = true
-          model.request.body.request = true
-          model.request.body.shared = "model"
+          model.settings = { model: true, shared: "model" }
+          model.headers = { model: "model", shared: "model" }
+          model.body = { model: true, shared: "model" }
         })
       })
 
       const model = required(yield* catalog.model.get(providerID, modelID))
-      expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
-      expect(model.request.body).toEqual({ provider: true, model: true, request: true, shared: "model" })
+      expect(model.settings).toEqual({ provider: true, shared: "model", model: true })
+      expect(model.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
+      expect(model.body).toEqual({ provider: true, shared: "model", model: true })
     }),
   )
 

+ 2 - 2
packages/core/test/config/agent.test.ts

@@ -62,7 +62,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
               info: decode({
                 permissions: [{ action: "read", resource: "*", effect: "allow" }],
                 agents: {
-                  reviewer: { variant: "high", hidden: true },
+                  reviewer: { model: "openrouter/openai/gpt-5#high", hidden: true },
                   removed: { disabled: true },
                   late: {
                     permissions: [{ action: "edit", resource: "*", effect: "allow" }],
@@ -170,7 +170,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
         hidden: true,
         color: "warning",
         steps: 12,
-        model: { providerID: "anthropic", id: "claude-sonnet", variant: undefined },
+        model: { providerID: "anthropic", id: "claude-sonnet" },
       })
       expect(reviewer.request).toEqual({
         settings: {},

+ 1 - 2
packages/core/test/config/command.test.ts

@@ -44,8 +44,7 @@ describe("ConfigCommandPlugin.Plugin", () => {
               `---
 description: File review
 agent: reviewer
-model: anthropic/claude
-variant: high
+model: anthropic/claude#high
 subtask: true
 ---
 Review files`,

+ 40 - 35
packages/core/test/config/config.test.ts

@@ -4,6 +4,7 @@ import { describe, expect } from "bun:test"
 import { Effect, Fiber, Layer, PubSub, Schema, Stream } from "effect"
 import { FastCheck } from "effect/testing"
 import { Config } from "@opencode-ai/core/config"
+import { ConfigModel } from "@opencode-ai/core/config/model"
 import { Config as ConfigSchema } from "@opencode-ai/schema/config"
 import { ConfigProvider } from "@opencode-ai/core/config/provider"
 import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@@ -16,12 +17,14 @@ import { EventV2 } from "@opencode-ai/core/event"
 import { Global } from "@opencode-ai/core/global"
 import { Location } from "@opencode-ai/core/location"
 import { Project } from "@opencode-ai/core/project"
+import { ProviderV2 } from "@opencode-ai/core/provider"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { location } from "../fixture/location"
 import { tmpdir } from "../fixture/tmpdir"
 import { testEffect } from "../lib/effect"
 
 const it = testEffect(Layer.empty)
+const selection = Schema.decodeUnknownSync(ConfigModel.Selection)
 
 function testLayer(
   directory: string,
@@ -47,11 +50,10 @@ function testLayer(
 }
 
 const provider = {
-  api: { type: "native", settings: {} },
-  request: {
-    headers: {},
-    body: {},
-  },
+  package: "native",
+  settings: {},
+  headers: {},
+  body: {},
   models: {},
 }
 
@@ -105,13 +107,19 @@ describe("Config", () => {
   it.effect("returns the latest defined scalar from priority-ordered documents", () =>
     Effect.sync(() => {
       const entries = [
-        new Config.Document({ type: "document", info: new Config.Info({ model: "openrouter/openai/gpt-5" }) }),
+        new Config.Document({
+          type: "document",
+          info: new Config.Info({ model: selection("openrouter/openai/gpt-5") }),
+        }),
         new Config.Directory({ type: "directory", path: AbsolutePath.make("/skills") }),
         new Config.Document({ type: "document", info: new Config.Info({}) }),
-        new Config.Document({ type: "document", info: new Config.Info({ model: "openrouter/openai/gpt-5.5" }) }),
+        new Config.Document({
+          type: "document",
+          info: new Config.Info({ model: selection("openrouter/openai/gpt-5.5") }),
+        }),
       ]
 
-      expect(Config.latest(entries, "model")).toBe("openrouter/openai/gpt-5.5")
+      expect(Config.latest(entries, "model")).toEqual(selection("openrouter/openai/gpt-5.5"))
       expect(Config.latest(entries, "default_agent")).toBeUndefined()
     }),
   )
@@ -142,7 +150,12 @@ describe("Config", () => {
     Effect.sync(() => {
       FastCheck.assert(
         FastCheck.property(Schema.toArbitrary(ConfigV1.Info), (info) => {
-          Schema.decodeUnknownSync(Config.Info)(ConfigMigrateV1.migrate(info), { errors: "all" })
+          const parsed = Schema.decodeUnknownSync(ConfigV1.Info)(
+            Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(
+              Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(info),
+            ),
+          )
+          Schema.decodeUnknownSync(Config.Info)(ConfigMigrateV1.migrate(parsed), { errors: "all" })
         }),
         { numRuns: 100 },
       )
@@ -165,12 +178,9 @@ describe("Config", () => {
         },
       })
 
-      expect(migrated.providers?.bedrock?.api).toEqual({
-        type: "aisdk",
-        package: "@ai-sdk/amazon-bedrock",
+      expect(migrated.providers?.bedrock).toMatchObject({
+        package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"),
         settings: { region: "us-east-1", profile: "dev" },
-      })
-      expect(migrated.providers?.bedrock?.request).toEqual({
         headers: { "x-test": "1" },
         body: { trace: true },
       })
@@ -197,8 +207,7 @@ describe("Config", () => {
           template: "Review changes",
           description: "Review code",
           agent: "reviewer",
-          model: "anthropic/claude",
-          variant: "high",
+          model: { providerID: "anthropic", model: "claude", variant: "high" },
           subtask: true,
         },
       })
@@ -431,8 +440,7 @@ describe("Config", () => {
                 ],
                 agents: {
                   reviewer: {
-                    model: "openrouter/openai/gpt-5",
-                    variant: "high",
+                    model: "openrouter/openai/gpt-5#high",
                     request: {
                       headers: { "x-agent": "reviewer" },
                       body: { reasoningEffort: "high" },
@@ -505,7 +513,7 @@ describe("Config", () => {
 
             expect(documents).toHaveLength(1)
             expect(documents[0]?.info.shell).toBe("/bin/bash")
-            expect(documents[0]?.info.model).toBe("anthropic/claude")
+            expect(documents[0]?.info.model).toEqual(selection("anthropic/claude"))
             expect(documents[0]?.info.default_agent).toBe("reviewer")
             expect(documents[0]?.info.autoupdate).toBe("notify")
             expect(documents[0]?.info.share).toBe("disabled")
@@ -516,8 +524,7 @@ describe("Config", () => {
               { action: "bash", resource: "git status", effect: "allow" },
             ])
             const reviewer = documents[0]?.info.agents?.reviewer
-            expect(reviewer?.model).toBe("openrouter/openai/gpt-5")
-            expect(reviewer?.variant).toBe("high")
+            expect(reviewer?.model).toEqual(selection("openrouter/openai/gpt-5#high"))
             expect(reviewer?.request).toEqual({
               headers: { "x-agent": "reviewer" },
               body: { reasoningEffort: "high" },
@@ -748,34 +755,32 @@ describe("Config", () => {
             })
             expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } })
             expect(documents[0]?.info.providers?.custom).toMatchObject({
-              request: { body: { apiKey: "secret" } },
+              settings: { apiKey: "secret" },
               models: {
                 model: {
-                  request: { body: { reasoningEffort: "high" } },
-                  variants: [{ id: "fast", body: { temperature: 0.2 } }],
+                  settings: { reasoningEffort: "high" },
+                  variants: [{ id: "fast", settings: { temperature: 0.2 } }],
                 },
               },
             })
             expect(documents[0]?.info.providers?.openai).toMatchObject({
-              api: { settings: {} },
-              request: { headers: { Authorization: "Bearer secret", "OpenAI-Organization": "org" } },
+              package: ProviderV2.aisdk("@ai-sdk/openai"),
+              settings: { apiKey: "secret", organization: "org" },
               models: {
                 model: {
-                  request: {
-                    body: { temperature: 0.3, reasoning: { effort: "high" }, service_tier: "priority" },
-                  },
-                  variants: [{ id: "high", body: { reasoning: { effort: "high", summary: "auto" } } }],
+                  settings: { temperature: 0.3, reasoningEffort: "high", serviceTier: "priority" },
+                  variants: [{ id: "high", settings: { reasoningEffort: "high", reasoningSummary: "auto" } }],
                 },
               },
             })
             expect(documents[0]?.info.providers?.anthropic).toMatchObject({
+              package: ProviderV2.aisdk("@ai-sdk/anthropic"),
               models: {
                 model: {
-                  request: {
-                    body: {
-                      output_config: { effort: "high", task_budget: 4096 },
-                      metadata: { user_id: "user-1" },
-                    },
+                  settings: {
+                    effort: "high",
+                    taskBudget: 4096,
+                    metadata: { userId: "user-1" },
                   },
                 },
               },

+ 28 - 0
packages/core/test/config/model.test.ts

@@ -0,0 +1,28 @@
+import { describe, expect, test } from "bun:test"
+import { ConfigModel } from "@opencode-ai/core/config/model"
+import { Model } from "@opencode-ai/schema/model"
+import { Provider } from "@opencode-ai/schema/provider"
+import { Schema } from "effect"
+
+const decode = Schema.decodeUnknownSync(ConfigModel.Selection)
+
+describe("ConfigModel.Selection", () => {
+  test("normalizes short and explicit model selections", () => {
+    expect(decode("openrouter/openai/gpt-5#high")).toEqual({
+      providerID: Provider.ID.make("openrouter"),
+      model: Model.ID.make("openai/gpt-5"),
+      variant: Model.VariantID.make("high"),
+    })
+    expect(decode({ providerID: "anthropic", model: "claude-sonnet", variant: "high" })).toEqual({
+      providerID: Provider.ID.make("anthropic"),
+      model: Model.ID.make("claude-sonnet"),
+      variant: Model.VariantID.make("high"),
+    })
+  })
+
+  test("rejects malformed selections and reserved fragments", () => {
+    expect(() => decode("gpt-5")).toThrow()
+    expect(() => decode("openai/gpt-5#")).toThrow()
+    expect(() => decode({ providerID: "openai", model: "gpt-5#high" })).toThrow()
+  })
+})

+ 20 - 196
packages/core/test/config/provider-options.test.ts

@@ -2,223 +2,47 @@ import { describe, expect, test } from "bun:test"
 import { ConfigProviderOptionsV1 } from "@opencode-ai/core/v1/config/provider-options"
 
 describe("ConfigProviderOptionsV1", () => {
-  test("keeps raw provider and request options unchanged", () => {
-    const lowerer = ConfigProviderOptionsV1.get("custom-provider")
-
-    expect(lowerer.provider({ apiKey: "secret", headers: { "x-test": "1" }, nested: { camelCase: true } })).toEqual({
-      body: { apiKey: "secret", headers: { "x-test": "1" }, nested: { camelCase: true } },
-    })
-    expect(lowerer.request({ nested: { camelCase: true } })).toEqual({ nested: { camelCase: true } })
-  })
-
-  test("falls back to raw lowering for prototype property package names", () => {
-    expect(ConfigProviderOptionsV1.get("toString").provider({ enabled: true })).toEqual({ body: { enabled: true } })
-  })
-
-  test("lowers OpenAI provider and request options", () => {
-    const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/openai")
-
+  test("splits provider overlays without changing package settings", () => {
     expect(
-      lowerer.provider({
+      ConfigProviderOptionsV1.provider({
         apiKey: "secret",
         baseURL: "https://openai.example/v1",
         organization: "org",
-        project: "project",
-        headers: { "x-test": "1" },
+        headers: { "x-test": "1", invalid: true },
         body: { store: true },
-        timeout: 1000,
-      }),
-    ).toEqual({
-      url: "https://openai.example/v1",
-      headers: {
-        Authorization: "Bearer secret",
-        "OpenAI-Organization": "org",
-        "OpenAI-Project": "project",
-        "x-test": "1",
-      },
-      body: { store: true },
-      settings: { timeout: 1000 },
-    })
-    expect(
-      lowerer.request({
-        reasoningEffort: "high",
-        reasoningSummary: "auto",
-        reasoning: { encryptedContent: true },
-        textVerbosity: "low",
-        text: { outputFormat: "plain" },
-        nestedValue: { camelCase: true },
-      }),
-    ).toEqual({
-      reasoning: { encrypted_content: true, effort: "high", summary: "auto" },
-      text: { output_format: "plain", verbosity: "low" },
-      nested_value: { camel_case: true },
-    })
-  })
-
-  test("lowers Anthropic provider and request options", () => {
-    const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/anthropic")
-
-    expect(
-      lowerer.provider({
-        apiKey: "secret",
-        authToken: "token",
-        baseURL: "https://anthropic.example",
-        headers: { "x-test": "1" },
-        body: { beta: true },
-        generateId: "custom",
+        nested: { camelCase: true },
       }),
     ).toEqual({
-      url: "https://anthropic.example",
-      headers: { "x-api-key": "secret", Authorization: "Bearer token", "x-test": "1" },
-      body: { beta: true },
-      settings: { generateId: "custom" },
-    })
-    expect(
-      lowerer.request({
-        effort: "high",
-        taskBudget: 1024,
-        metadata: { userId: "user", traceId: "trace" },
-        nestedValue: { camelCase: true },
-      }),
-    ).toEqual({
-      output_config: { effort: "high", task_budget: 1024 },
-      metadata: { user_id: "user", trace_id: "trace" },
-      nested_value: { camel_case: true },
-    })
-  })
-
-  test("lowers Google provider and request options", () => {
-    const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/google")
-
-    expect(
-      lowerer.provider({
+      settings: {
         apiKey: "secret",
-        baseURL: "https://google.example",
-        headers: { "x-test": "1" },
-        body: { trace: true },
-        project: "project",
-      }),
-    ).toEqual({
-      url: "https://google.example",
-      headers: { "x-goog-api-key": "secret", "x-test": "1" },
-      body: { trace: true },
-      settings: { project: "project" },
-    })
-    expect(
-      lowerer.request({
-        thinkingConfig: { thinkingBudget: 1024 },
-        responseModalities: ["TEXT"],
-        mediaResolution: "high",
-        imageConfig: { aspectRatio: "16:9" },
-        safetySettings: ["safe"],
-      }),
-    ).toEqual({
-      safetySettings: ["safe"],
-      generationConfig: {
-        thinkingConfig: { thinkingBudget: 1024 },
-        responseModalities: ["TEXT"],
-        mediaResolution: "high",
-        imageConfig: { aspectRatio: "16:9" },
+        baseURL: "https://openai.example/v1",
+        organization: "org",
+        nested: { camelCase: true },
       },
-    })
-  })
-
-  test("lowers Azure provider options and uses OpenAI request lowering", () => {
-    const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/azure")
-
-    expect(
-      lowerer.provider({
-        apiKey: "secret",
-        baseURL: "https://azure.example",
-        headers: { "x-test": "1" },
-        body: { trace: true },
-        resourceName: "resource",
-      }),
-    ).toEqual({
-      url: "https://azure.example",
-      headers: { "api-key": "secret", "x-test": "1" },
-      body: { trace: true },
-      settings: { resourceName: "resource" },
-    })
-    expect(lowerer.request({ reasoningEffort: "high", reasoningSummary: "auto", textVerbosity: "low" })).toEqual({
-      reasoning: { effort: "high", summary: "auto" },
-      text: { verbosity: "low" },
-    })
-  })
-
-  test("lowers Amazon Bedrock provider and request options", () => {
-    const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/amazon-bedrock")
-
-    expect(
-      lowerer.provider({
-        headers: { "x-test": "1" },
-        body: { trace: true },
-        region: "us-east-1",
-        profile: "dev",
-      }),
-    ).toEqual({
       headers: { "x-test": "1" },
-      body: { trace: true },
-      settings: { region: "us-east-1", profile: "dev" },
-    })
-    expect(lowerer.request({ temperature: 0.2 })).toEqual({
-      additionalModelRequestFields: { temperature: 0.2 },
+      body: { store: true },
     })
   })
 
-  test("lowers OpenAI-compatible provider and request options", () => {
-    const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/openai-compatible")
-
+  test("keeps model and variant options unchanged", () => {
     expect(
-      lowerer.provider({
-        baseURL: "https://compatible.example/v1",
-        headers: { "x-test": "1" },
-        body: { trace: true },
-        apiKey: "secret",
+      ConfigProviderOptionsV1.model({
+        reasoningEffort: "high",
+        taskBudget: 1024,
+        metadata: { userId: "user" },
       }),
     ).toEqual({
-      url: "https://compatible.example/v1",
-      headers: { "x-test": "1" },
-      body: { trace: true },
-      settings: { apiKey: "secret" },
-    })
-    expect(lowerer.request({ reasoningEffort: "high", serviceTier: "priority" })).toEqual({
-      reasoning_effort: "high",
-      serviceTier: "priority",
+      reasoningEffort: "high",
+      taskBudget: 1024,
+      metadata: { userId: "user" },
     })
   })
 
-  test.each([
-    "@ai-sdk/cerebras",
-    "@ai-sdk/deepinfra",
-    "@ai-sdk/groq",
-    "@ai-sdk/mistral",
-    "@ai-sdk/togetherai",
-    "@ai-sdk/xai",
-    "@openrouter/ai-sdk-provider",
-    "ai-gateway-provider",
-    "venice-ai-sdk-provider",
-  ])("uses OpenAI-compatible lowering for %s", (packageName) => {
-    const lowerer = ConfigProviderOptionsV1.get(packageName)
-
-    expect(lowerer.provider({ baseURL: "https://example.test", apiKey: "secret" })).toEqual({
-      url: "https://example.test",
+  test("uses mechanical lowering for custom provider options", () => {
+    expect(ConfigProviderOptionsV1.provider({ enabled: true })).toEqual({
+      settings: { enabled: true },
       headers: undefined,
       body: undefined,
-      settings: { apiKey: "secret" },
     })
-    expect(lowerer.request({ reasoningEffort: "high" })).toEqual({ reasoning_effort: "high" })
   })
-
-  test.each(["@ai-sdk/google-vertex", "@ai-sdk/google-vertex/anthropic"])(
-    "uses provider family lowering for %s",
-    (packageName) => {
-      const lowerer = ConfigProviderOptionsV1.get(packageName)
-
-      expect(lowerer.provider({ baseURL: "https://example.test", profile: "dev" })).toMatchObject({
-        url: "https://example.test",
-        settings: { profile: "dev" },
-      })
-    },
-  )
 })

+ 24 - 25
packages/core/test/config/provider.test.ts

@@ -45,13 +45,6 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
   )
 }
 
-function request(headers: Record<string, string>, variant?: string) {
-  return {
-    headers,
-    variant,
-  }
-}
-
 const decode = Schema.decodeUnknownSync(Config.Info)
 
 describe("ConfigProviderPlugin.Plugin", () => {
@@ -68,7 +61,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
               info: decode({
                 providers: {
                   opencode: {
-                    api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" },
+                    package: "aisdk:@ai-sdk/openai",
+                    settings: { baseURL: "https://opencode.test/v1" },
                     models: {
                       "alpha-gpt-next": {
                         variants: [
@@ -119,7 +113,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
               info: decode({
                 providers: {
                   opencode: {
-                    api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" },
+                    package: "aisdk:@ai-sdk/openai",
+                    settings: { baseURL: "https://opencode.test/v1" },
                   },
                 },
               }),
@@ -144,7 +139,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
       yield* addPlugin(config)
 
       const model = required(yield* catalog.model.get(providerID, modelID))
-      expect(model.variants[0]).toMatchObject({
+      expect(model.variants?.[0]).toMatchObject({
         id: "high",
         body: { reasoningEffort: "high" },
       })
@@ -169,8 +164,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
                     custom: {
                       name: "Configured",
                       env: ["CUSTOM_API_KEY"],
-                      api: { type: "native", settings: {} },
-                      request: request({ first: "first", shared: "first" }),
+                      package: "native",
+                      headers: { first: "first", shared: "first" },
                       models: {
                         chat: {
                           name: "First",
@@ -178,7 +173,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
                           disabled: true,
                           limit: { context: 100, output: 50 },
                           cost: { input: 1, output: 2 },
-                          request: request({ first: "first", shared: "first" }, "retained"),
+                          settings: { retained: true },
+                          headers: { first: "first", shared: "first" },
                           variants: [
                             {
                               id: "fast",
@@ -197,17 +193,18 @@ describe("ConfigProviderPlugin.Plugin", () => {
                   model: "custom/default",
                   providers: {
                     custom: {
-                      api: { type: "aisdk", package: "custom-sdk", url: "https://example.test" },
-                      request: request({ last: "last", shared: "last" }),
+                      package: "aisdk:custom-sdk",
+                      settings: { baseURL: "https://example.test" },
+                      headers: { last: "last", shared: "last" },
                       models: {
                         default: {
                           name: "Default",
                         },
                         chat: {
-                          api: { id: "api-chat" },
+                          modelID: "api-chat",
                           name: "Last",
                           limit: { output: 75 },
-                          request: request({ last: "last", shared: "last" }),
+                          headers: { last: "last", shared: "last" },
                           variants: [
                             {
                               id: "fast",
@@ -247,22 +244,24 @@ describe("ConfigProviderPlugin.Plugin", () => {
         })
         expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed")
         expect(provider.disabled).toBeUndefined()
-        expect(provider.api).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" })
-        expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
-        expect(model.api.id).toBe(ModelV2.ID.make("api-chat"))
+        expect(provider.package).toBe("aisdk:custom-sdk")
+        expect(provider.settings).toEqual({ baseURL: "https://example.test" })
+        expect(provider.headers).toEqual({ first: "first", shared: "last", last: "last" })
+        expect(model.id).toBe(modelID)
+        expect(model.modelID).toBe(ModelV2.ID.make("api-chat"))
         expect(model.name).toBe("Last")
         expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
         expect(model.enabled).toBe(false)
         expect(model.limit).toEqual({ context: 100, output: 75 })
         expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
-        expect(model.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
-        expect(model.request.variant).toBe("retained")
-        expect(model.variants.map((variant) => variant.id)).toEqual([
+        expect(model.settings).toEqual({ baseURL: "https://example.test", retained: true })
+        expect(model.headers).toEqual({ first: "first", shared: "last", last: "last" })
+        expect(model.variants?.map((variant) => variant.id)).toEqual([
           ModelV2.VariantID.make("fast"),
           ModelV2.VariantID.make("slow"),
         ])
-        expect(model.variants[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
-        expect(model.variants[1]?.headers).toEqual({ slow: "slow" })
+        expect(model.variants?.[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
+        expect(model.variants?.[1]?.headers).toEqual({ slow: "slow" })
       }),
     ),
   )

+ 6 - 6
packages/core/test/location-layer.test.ts

@@ -271,7 +271,7 @@ describe("LocationServiceMap", () => {
                 providers: {
                   unavailable: {
                     name: "Unavailable",
-                    api: { type: "native", settings: {} },
+                    package: "test-provider",
                     models: { chat: { disabled: true } },
                   },
                 },
@@ -306,7 +306,7 @@ describe("LocationServiceMap", () => {
     ),
   )
 
-  it.live("preserves the selected catalog identity when the api model id differs", () =>
+  it.live("preserves the selected catalog identity when the package model id differs", () =>
     Effect.acquireRelease(
       Effect.promise(() => tmpdir()),
       (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
@@ -318,12 +318,12 @@ describe("LocationServiceMap", () => {
             const catalog = yield* Catalog.Service
             yield* catalog.transform((editor) => {
               editor.provider.update(ProviderV2.ID.make("aliased"), (provider) => {
-                provider.api = { type: "aisdk", package: "@ai-sdk/openai", settings: {} }
+                provider.package = ProviderV2.aisdk("@ai-sdk/openai")
               })
               editor.model.update(ProviderV2.ID.make("aliased"), ModelV2.ID.make("fast"), (model) => {
-                // Catalog id and provider API id intentionally differ, like gpt-5.5-fast -> gpt-5.5.
-                model.api = { ...model.api, id: ModelV2.ID.make("base") }
-                model.variants.push({ id: ModelV2.VariantID.make("high"), settings: {}, headers: {}, body: {} })
+                // Catalog id and package model id intentionally differ, like gpt-5.5-fast -> gpt-5.5.
+                model.modelID = ModelV2.ID.make("base")
+                model.variants = [{ id: ModelV2.VariantID.make("high") }]
               })
             })
             const models = yield* SessionRunnerModel.Service

+ 4 - 6
packages/core/test/plugin/fixtures/variant-source-plugin.ts

@@ -1,4 +1,5 @@
 import { define } from "@opencode-ai/plugin/v2/effect"
+import { ProviderV2 } from "@opencode-ai/core/provider"
 import { Effect } from "effect"
 
 export default define({
@@ -7,14 +8,11 @@ export default define({
     ctx.catalog
       .transform((catalog) => {
         catalog.provider.update("configured", (provider) => {
-          provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
         })
         catalog.model.update("configured", "glm-5.2", (model) => {
-          model.api = {
-            id: "glm-5.2",
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-          }
+          model.modelID = "glm-5.2"
+          model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
           model.variants = [
             {
               id: "high",

+ 11 - 18
packages/core/test/plugin/host.ts

@@ -162,7 +162,7 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"
                   id: ModelV2.ID.make(current.id),
                   providerID: ProviderV2.ID.make(current.providerID),
                   family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
-                  variants: current.variants.map((variant) => ({
+                  variants: current.variants?.map((variant) => ({
                     ...variant,
                     id: ModelV2.VariantID.make(variant.id),
                   })),
@@ -327,35 +327,28 @@ function agentInfo(value: AgentV2.Info) {
 function providerInfo(value: ProviderV2.MutableInfo) {
   return {
     ...value,
-    api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
-    request: {
-      settings: { ...value.request.settings },
-      headers: { ...value.request.headers },
-      body: { ...value.request.body },
-    },
+    settings: value.settings && { ...value.settings },
+    headers: value.headers && { ...value.headers },
+    body: value.body && { ...value.body },
   }
 }
 
 function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
   return {
     ...value,
-    api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
+    settings: value.settings && { ...value.settings },
+    headers: value.headers && { ...value.headers },
+    body: value.body && { ...value.body },
     capabilities: {
       ...value.capabilities,
       input: [...value.capabilities.input],
       output: [...value.capabilities.output],
     },
-    request: {
-      ...value.request,
-      settings: { ...value.request.settings },
-      headers: { ...value.request.headers },
-      body: { ...value.request.body },
-    },
-    variants: value.variants.map((variant) => ({
+    variants: value.variants?.map((variant) => ({
       ...variant,
-      settings: { ...variant.settings },
-      headers: { ...variant.headers },
-      body: { ...variant.body },
+      settings: variant.settings && { ...variant.settings },
+      headers: variant.headers && { ...variant.headers },
+      body: variant.body && { ...variant.body },
     })),
     time: { ...value.time },
     cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),

+ 14 - 23
packages/core/test/plugin/models-dev.test.ts

@@ -94,16 +94,16 @@ describe("ModelsDevPlugin", () => {
       const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast"))
 
       expect(base?.variants).toEqual([])
-      expect(base?.request.body).toEqual({})
+      expect(base?.body).toEqual({})
       expect(fast).toMatchObject({
         id: "gpt-5.4-fast",
+        modelID: "gpt-5.4",
         providerID: "acme",
         name: "GPT-5.4 Fast",
-        api: { id: "gpt-5.4" },
-        request: {
-          headers: { "x-mode": "fast" },
-          body: { service_tier: "priority" },
-        },
+        package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
+        settings: { baseURL: "https://api.acme.test/v1" },
+        headers: { "x-mode": "fast" },
+        body: { service_tier: "priority" },
         variants: [],
       })
       expect(fast?.cost).toEqual([
@@ -191,7 +191,7 @@ describe("ModelsDevPlugin", () => {
           )
 
           const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning"))
-          expect(model?.variants.map((variant) => variant.id)).toEqual([
+          expect(model?.variants?.map((variant) => variant.id)).toEqual([
             ModelV2.VariantID.make("low"),
             ModelV2.VariantID.make("high"),
           ])
@@ -202,8 +202,6 @@ describe("ModelsDevPlugin", () => {
               reasoningSummary: "auto",
               include: ["reasoning.encrypted_content"],
             },
-            headers: {},
-            body: {},
           })
           expect(model?.variants).toContainEqual({
             id: ModelV2.VariantID.make("high"),
@@ -212,20 +210,16 @@ describe("ModelsDevPlugin", () => {
               reasoningSummary: "auto",
               include: ["reasoning.encrypted_content"],
             },
-            headers: {},
-            body: {},
           })
 
           const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high"))
           expect(mode).toMatchObject({
             id: "gpt-reasoning-high",
             name: "GPT Reasoning High",
-            request: {
-              headers: { "x-mode": "high" },
-              body: { service_tier: "priority" },
-            },
+            headers: { "x-mode": "high" },
+            body: { service_tier: "priority" },
           })
-          expect(mode?.variants.map((variant) => variant.id)).toEqual([
+          expect(mode?.variants?.map((variant) => variant.id)).toEqual([
             ModelV2.VariantID.make("low"),
             ModelV2.VariantID.make("high"),
           ])
@@ -234,22 +228,19 @@ describe("ModelsDevPlugin", () => {
           expect(budgetModel?.variants).toContainEqual({
             id: ModelV2.VariantID.make("high"),
             settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
-            headers: {},
-            body: {},
           })
           expect(budgetModel?.variants).toContainEqual({
             id: ModelV2.VariantID.make("max"),
             settings: { thinking: { type: "enabled", budgetTokens: 64000 } },
-            headers: {},
-            body: {},
           })
 
-          const anthropicEffortModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-effort"))
+          const anthropicEffortModel = yield* catalog.model.get(
+            ProviderV2.ID.anthropic,
+            ModelV2.ID.make("claude-effort"),
+          )
           expect(anthropicEffortModel?.variants).toContainEqual({
             id: ModelV2.VariantID.make("low"),
             settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
-            headers: {},
-            body: {},
           })
         }).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
       (previous) =>

+ 10 - 6
packages/core/test/plugin/provider-alibaba.test.ts

@@ -28,7 +28,8 @@ describe("AlibabaPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
-          api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("qwen"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/alibaba",
         options: { name: "alibaba" },
@@ -45,7 +46,8 @@ describe("AlibabaPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
-          api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("qwen"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "alibaba" },
@@ -62,7 +64,8 @@ describe("AlibabaPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
-          api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("qwen"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/alibaba",
         options: { name: "custom-alibaba", apiKey: "test" },
@@ -74,17 +77,18 @@ describe("AlibabaPlugin", () => {
     }),
   )
 
-  it.effect("uses the old default languageModel(api.id) behavior", () =>
+  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")),
-        api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" },
+        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.api.id)
+      const language = result.sdk?.languageModel(item.modelID ?? item.id)
       expect(language?.modelId).toBe("qwen-plus")
       expect(language?.provider).toBe("alibaba.chat")
     }),

+ 49 - 63
packages/core/test/plugin/provider-amazon-bedrock.test.ts

@@ -79,31 +79,24 @@ function openAIUrl(language: unknown, path: string, modelId: string) {
 }
 
 describe("AmazonBedrockPlugin", () => {
-  it.effect("moves endpoint option to api URL", () =>
+  it.effect("moves endpoint setting to baseURL", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         const bedrock = ProviderV2.Info.make({
           ...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock),
-          api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
-          request: {
-            headers: {},
-            body: { endpoint: "https://bedrock.example" },
-          },
+          package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"),
+          settings: { endpoint: "https://bedrock.example" },
         })
         catalog.provider.update(bedrock.id, (item) => {
-          item.api = bedrock.api
-          item.request = { settings: {}, headers: {}, body: { endpoint: "https://bedrock.example" } }
+          item.package = bedrock.package
+          item.settings = { endpoint: "https://bedrock.example" }
         })
       })
       yield* addPlugin()
       const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
-      expect(result.api).toEqual({
-        type: "aisdk",
-        package: "@ai-sdk/amazon-bedrock",
-        url: "https://bedrock.example",
-      })
-      expect(result.request.body.endpoint).toBeUndefined()
+      expect(result.package).toBe(ProviderV2.aisdk("@ai-sdk/amazon-bedrock"))
+      expect(result.settings).toEqual({ baseURL: "https://bedrock.example" })
     }),
   )
 
@@ -116,7 +109,8 @@ describe("AmazonBedrockPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+            package: ProviderV2.aisdk("test-provider"),
           }),
           package: "@ai-sdk/amazon-bedrock",
           options: {
@@ -141,7 +135,8 @@ describe("AmazonBedrockPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+            package: ProviderV2.aisdk("test-provider"),
           }),
           package: "@ai-sdk/amazon-bedrock",
           options: {
@@ -175,11 +170,8 @@ describe("AmazonBedrockPlugin", () => {
           const result = yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-              api: {
-                id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
-                type: "aisdk",
-                package: "test-provider",
-              },
+              modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+              package: ProviderV2.aisdk("test-provider"),
             }),
             package: "@ai-sdk/amazon-bedrock",
             options: { name: "amazon-bedrock" },
@@ -199,7 +191,8 @@ describe("AmazonBedrockPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+            package: ProviderV2.aisdk("test-provider"),
           }),
           package: "@ai-sdk/amazon-bedrock",
           options: { name: "amazon-bedrock", region: "eu-west-1" },
@@ -218,7 +211,8 @@ describe("AmazonBedrockPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+            package: ProviderV2.aisdk("test-provider"),
           }),
           package: "@ai-sdk/amazon-bedrock",
           options: { name: "amazon-bedrock" },
@@ -237,7 +231,8 @@ describe("AmazonBedrockPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+            package: ProviderV2.aisdk("test-provider"),
           }),
           package: "@ai-sdk/amazon-bedrock",
           options: { name: "amazon-bedrock" },
@@ -257,7 +252,8 @@ describe("AmazonBedrockPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+            package: ProviderV2.aisdk("test-provider"),
           }),
           package: "@ai-sdk/amazon-bedrock",
           options: {
@@ -286,7 +282,8 @@ describe("AmazonBedrockPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+            package: ProviderV2.aisdk("test-provider"),
           }),
           package: "@ai-sdk/amazon-bedrock",
           options: {
@@ -314,11 +311,8 @@ describe("AmazonBedrockPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
-            api: {
-              id: ModelV2.ID.make("openai.gpt-5.5"),
-              type: "aisdk",
-              package: "@ai-sdk/amazon-bedrock/mantle",
-            },
+            modelID: ModelV2.ID.make("openai.gpt-5.5"),
+            package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
           }),
           package: "@ai-sdk/amazon-bedrock/mantle",
           options: {
@@ -345,11 +339,8 @@ describe("AmazonBedrockPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
-          api: {
-            id: ModelV2.ID.make("openai.gpt-5.5"),
-            type: "aisdk",
-            package: "@ai-sdk/amazon-bedrock/mantle",
-          },
+          modelID: ModelV2.ID.make("openai.gpt-5.5"),
+          package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
         }),
         sdk: fakeSelectorSdk(calls),
         options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
@@ -357,11 +348,8 @@ describe("AmazonBedrockPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
-          api: {
-            id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
-            type: "aisdk",
-            package: "@ai-sdk/amazon-bedrock/mantle",
-          },
+          modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
+          package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
         }),
         sdk: fakeSelectorSdk(calls),
         options: { region: "us-east-1" },
@@ -378,11 +366,8 @@ describe("AmazonBedrockPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-          api: {
-            id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
-            type: "aisdk",
-            package: "@ai-sdk/amazon-bedrock/anthropic",
-          },
+          modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+          package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/anthropic"),
         }),
         package: "@ai-sdk/amazon-bedrock/anthropic",
         options: { name: "amazon-bedrock" },
@@ -409,11 +394,8 @@ describe("AmazonBedrockPlugin", () => {
           const result = yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-              api: {
-                id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
-                type: "aisdk",
-                package: "test-provider",
-              },
+              modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+              package: ProviderV2.aisdk("test-provider"),
             }),
             package: "@ai-sdk/amazon-bedrock",
             options: {
@@ -444,7 +426,8 @@ describe("AmazonBedrockPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-          api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
         options: {},
@@ -452,7 +435,8 @@ describe("AmazonBedrockPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-          api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
         options: { region: "eu-west-1" },
@@ -460,11 +444,8 @@ describe("AmazonBedrockPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
-          api: {
-            id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
-            type: "aisdk",
-            package: "test-provider",
-          },
+          modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
         options: { region: "eu-west-1" },
@@ -472,7 +453,8 @@ describe("AmazonBedrockPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-          api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
         options: { region: "ap-northeast-1" },
@@ -480,7 +462,8 @@ describe("AmazonBedrockPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-          api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
         options: { region: "ap-southeast-2" },
@@ -505,7 +488,8 @@ describe("AmazonBedrockPlugin", () => {
         yield* aisdk.runLanguage({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+            package: ProviderV2.aisdk("test-provider"),
           }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: {},
@@ -591,7 +575,8 @@ describe("AmazonBedrockPlugin", () => {
         yield* aisdk.runLanguage({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
-            api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make(item.modelID),
+            package: ProviderV2.aisdk("test-provider"),
           }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: { region: item.region },
@@ -610,7 +595,8 @@ describe("AmazonBedrockPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
-          api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
         options: { region: "eu-west-1" },

+ 11 - 11
packages/core/test/plugin/provider-anthropic.test.ts

@@ -31,19 +31,19 @@ describe("AnthropicPlugin", () => {
       yield* catalog.transform((catalog) => {
         const item = ProviderV2.Info.make({
           ...ProviderV2.Info.empty(ProviderV2.ID.anthropic),
-          api: { type: "aisdk", package: "@ai-sdk/anthropic" },
-          request: { headers: { Existing: "1" }, body: {} },
+          package: ProviderV2.aisdk("@ai-sdk/anthropic"),
+          headers: { Existing: "1" },
         })
         catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
-          draft.request = { settings: {}, headers: { Existing: "1" }, body: {} }
+          draft.package = item.package
+          draft.headers = { Existing: "1" }
         })
       })
       yield* addPlugin()
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.["anthropic-beta"]).toBe(
         "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
       )
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.Existing).toBe("1")
     }),
   )
 
@@ -52,9 +52,7 @@ describe("AnthropicPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {}))
       yield* addPlugin()
-      expect(
-        required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"],
-      ).toBeUndefined()
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).headers?.["anthropic-beta"]).toBeUndefined()
     }),
   )
 
@@ -66,7 +64,8 @@ describe("AnthropicPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")),
-          api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
+          modelID: ModelV2.ID.make("claude-sonnet-4-5"),
+          package: ProviderV2.aisdk("@ai-sdk/anthropic"),
         }),
         package: "@ai-sdk/anthropic",
         options: { name: "custom-anthropic", apiKey: "test" },
@@ -83,7 +82,8 @@ describe("AnthropicPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
-          api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
+          modelID: ModelV2.ID.make("claude-sonnet-4-5"),
+          package: ProviderV2.aisdk("@ai-sdk/anthropic"),
         }),
         package: "@ai-sdk/anthropic",
         options: { name: "anthropic", apiKey: "test" },

+ 27 - 21
packages/core/test/plugin/provider-azure-cognitive-services.test.ts

@@ -66,18 +66,16 @@ describe("AzureCognitiveServicesPlugin", () => {
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) => {
           catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
-            item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
+            item.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
           })
         })
         yield* addPlugin()
         const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
-        expect(result.api).toEqual({
-          type: "aisdk",
-          package: "@ai-sdk/openai-compatible",
-          url: "https://cognitive.cognitiveservices.azure.com/openai",
+        expect(result).toMatchObject({
+          package: "aisdk:@ai-sdk/openai-compatible",
+          settings: { baseURL: "https://cognitive.cognitiveservices.azure.com/openai" },
         })
-        expect(result.request.body.baseURL).toBeUndefined()
-        expect(result.request.body.resourceName).toBeUndefined()
+        expect(result.settings?.resourceName).toBeUndefined()
       }),
     ),
   )
@@ -89,26 +87,28 @@ describe("AzureCognitiveServicesPlugin", () => {
         yield* catalog.transform((catalog) => {
           const azure = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")),
-            api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
+            package: "aisdk:@ai-sdk/openai-compatible",
           })
           const openai = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.openai),
-            api: { type: "aisdk", package: "test-provider" },
+            package: "aisdk:test-provider",
           })
           catalog.provider.update(azure.id, (item) => {
-            item.api = azure.api
+            item.package = azure.package
+            item.package = azure.package
           })
           catalog.provider.update(openai.id, (item) => {
-            item.api = openai.api
+            item.package = openai.package
+            item.package = openai.package
           })
         })
         yield* addPlugin()
         const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
         const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
-        expect(azure.request.body.baseURL).toBeUndefined()
-        expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
-        expect(openai.request.body.baseURL).toBeUndefined()
-        expect(openai.api).toEqual({ type: "aisdk", package: "test-provider" })
+        expect(azure.settings?.baseURL).toBeUndefined()
+        expect(azure).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible" })
+        expect(openai.settings?.baseURL).toBeUndefined()
+        expect(openai).toMatchObject({ package: "aisdk:test-provider" })
       }),
     ),
   )
@@ -122,7 +122,8 @@ describe("AzureCognitiveServicesPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
-          api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("deployment"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: { useCompletionUrls: true },
@@ -140,7 +141,8 @@ describe("AzureCognitiveServicesPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
-          api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("deployment"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -148,7 +150,8 @@ describe("AzureCognitiveServicesPlugin", () => {
       const ignored = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
-          api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("deployment"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -168,7 +171,8 @@ describe("AzureCognitiveServicesPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")),
-          api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("messages-deployment"),
+          package: "aisdk:test-provider",
         }),
         sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
         options: {},
@@ -176,7 +180,8 @@ describe("AzureCognitiveServicesPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
-          api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("chat-deployment"),
+          package: "aisdk:test-provider",
         }),
         sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
         options: {},
@@ -184,7 +189,8 @@ describe("AzureCognitiveServicesPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")),
-          api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("language-deployment"),
+          package: "aisdk:test-provider",
         }),
         sdk: { languageModel: sdk.languageModel },
         options: {},

+ 37 - 28
packages/core/test/plugin/provider-azure.test.ts

@@ -66,11 +66,11 @@ describe("AzurePlugin", () => {
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) => {
           catalog.provider.update(ProviderV2.ID.azure, (item) => {
-            item.api = { type: "aisdk", package: "@ai-sdk/azure" }
+            item.package = ProviderV2.aisdk("@ai-sdk/azure")
           })
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
       }),
     ),
   )
@@ -82,18 +82,18 @@ describe("AzurePlugin", () => {
         yield* catalog.transform((catalog) => {
           const azure = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.azure),
-            api: { type: "aisdk", package: "@ai-sdk/azure" },
-            request: { headers: {}, body: { resourceName: "from-config" } },
+            package: ProviderV2.aisdk("@ai-sdk/azure"),
+            settings: { resourceName: "from-config" },
           })
           catalog.provider.update(azure.id, (item) => {
-            item.api = azure.api
-            item.request = { settings: {}, headers: {}, body: { resourceName: "from-config" } }
+            item.package = azure.package
+            item.settings = { resourceName: "from-config" }
           })
           catalog.provider.update(ProviderV2.ID.openai, () => {})
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-config")
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.resourceName).toBeUndefined()
       }),
     ),
   )
@@ -105,16 +105,16 @@ describe("AzurePlugin", () => {
         yield* catalog.transform((catalog) => {
           const azure = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.azure),
-            api: { type: "aisdk", package: "@ai-sdk/azure" },
-            request: { headers: {}, body: { resourceName: "" } },
+            package: ProviderV2.aisdk("@ai-sdk/azure"),
+            settings: { resourceName: "" },
           })
           catalog.provider.update(azure.id, (item) => {
-            item.api = azure.api
-            item.request = { settings: {}, headers: {}, body: { resourceName: "" } }
+            item.package = azure.package
+            item.settings = { resourceName: "" }
           })
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
       }),
     ),
   )
@@ -126,16 +126,16 @@ describe("AzurePlugin", () => {
         yield* catalog.transform((catalog) => {
           const azure = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.azure),
-            api: { type: "aisdk", package: "@ai-sdk/azure" },
-            request: { headers: {}, body: { resourceName: "   " } },
+            package: ProviderV2.aisdk("@ai-sdk/azure"),
+            settings: { resourceName: "   " },
           })
           catalog.provider.update(azure.id, (item) => {
-            item.api = azure.api
-            item.request = { settings: {}, headers: {}, body: { resourceName: "   " } }
+            item.package = azure.package
+            item.settings = { resourceName: "   " }
           })
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
       }),
     ),
   )
@@ -149,7 +149,8 @@ describe("AzurePlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
-            api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("deployment"),
+            package: ProviderV2.aisdk("test-provider"),
           }),
           package: "@ai-sdk/azure",
           options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
@@ -168,7 +169,8 @@ describe("AzurePlugin", () => {
           .runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
-              api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+              modelID: ModelV2.ID.make("deployment"),
+              package: ProviderV2.aisdk("test-provider"),
             }),
             package: "@ai-sdk/azure",
             options: { name: "azure" },
@@ -188,7 +190,8 @@ describe("AzurePlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
-          api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("deployment"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: fakeSelectorSdk(calls),
         options: { useCompletionUrls: true },
@@ -206,7 +209,8 @@ describe("AzurePlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
-          api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("deployment"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: fakeSelectorSdk(calls),
         options: { useCompletionUrls: true },
@@ -224,8 +228,9 @@ describe("AzurePlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
-          api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
-          request: { headers: {}, body: { useCompletionUrls: true } },
+          modelID: ModelV2.ID.make("deployment"),
+          package: ProviderV2.aisdk("test-provider"),
+          body: { useCompletionUrls: true },
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -243,7 +248,8 @@ describe("AzurePlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
-          api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("deployment"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -251,7 +257,8 @@ describe("AzurePlugin", () => {
       const ignored = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
-          api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("deployment"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -274,7 +281,8 @@ describe("AzurePlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
-          api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("messages-deployment"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
         options: {},
@@ -282,7 +290,8 @@ describe("AzurePlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
-          api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("language-deployment"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: { languageModel: make("languageModel") },
         options: {},

+ 10 - 19
packages/core/test/plugin/provider-cerebras.test.ts

@@ -36,12 +36,12 @@ describe("CerebrasPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
-          item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
-          item.request.headers.Existing = "1"
+          item.package = ProviderV2.aisdk("@ai-sdk/cerebras")
+          item.headers = { ...item.headers, Existing: "1" }
         })
       })
       yield* addPlugin()
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.headers).toEqual({
         Existing: "1",
         "X-Cerebras-3rd-Party-Integration": "opencode",
       })
@@ -53,7 +53,7 @@ describe("CerebrasPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
       yield* addPlugin()
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.headers).toBeUndefined()
     }),
   )
 
@@ -69,11 +69,8 @@ describe("CerebrasPlugin", () => {
             ProviderV2.ID.make("custom-cerebras"),
             ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
           ),
-          api: {
-            id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
-            type: "aisdk",
-            package: "test-provider",
-          },
+          modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/cerebras",
         options: { name: "custom-cerebras", apiKey: "test" },
@@ -95,11 +92,8 @@ describe("CerebrasPlugin", () => {
             ProviderV2.ID.make("custom-cerebras"),
             ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
           ),
-          api: {
-            id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
-            type: "aisdk",
-            package: "test-provider",
-          },
+          modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/cerebras",
         options: { name: "configured-cerebras", apiKey: "test" },
@@ -120,11 +114,8 @@ describe("CerebrasPlugin", () => {
             ProviderV2.ID.make("custom-cerebras"),
             ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
           ),
-          api: {
-            id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
-            type: "aisdk",
-            package: "test-provider",
-          },
+          modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/groq",
         options: { name: "custom-cerebras", apiKey: "test" },

+ 22 - 15
packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts

@@ -118,7 +118,8 @@ describe("CloudflareAIGatewayPlugin", () => {
           const result = yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
-              api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+              modelID: ModelV2.ID.make("openai/gpt-5"),
+              package: "aisdk:test-provider",
             }),
             package: "ai-gateway-provider",
             options: { name: "cloudflare-ai-gateway" },
@@ -139,7 +140,8 @@ describe("CloudflareAIGatewayPlugin", () => {
         yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
-            api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("openai/gpt-5"),
+            package: "aisdk:test-provider",
           }),
           package: "ai-gateway-provider",
           options: {
@@ -183,7 +185,8 @@ describe("CloudflareAIGatewayPlugin", () => {
         yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
-            api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("openai/gpt-5"),
+            package: "aisdk:test-provider",
           }),
           package: "ai-gateway-provider",
           options: {
@@ -212,7 +215,8 @@ describe("CloudflareAIGatewayPlugin", () => {
         yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
-            api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("openai/gpt-5"),
+            package: "aisdk:test-provider",
           }),
           package: "ai-gateway-provider",
           options: {
@@ -249,7 +253,8 @@ describe("CloudflareAIGatewayPlugin", () => {
           yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
-              api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+              modelID: ModelV2.ID.make("openai/gpt-5"),
+              package: "aisdk:test-provider",
             }),
             package: "ai-gateway-provider",
             options: {
@@ -280,7 +285,8 @@ describe("CloudflareAIGatewayPlugin", () => {
         yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
-            api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("openai/gpt-5"),
+            package: "aisdk:test-provider",
           }),
           package: "ai-gateway-provider",
           options: { name: "cloudflare-ai-gateway" },
@@ -302,7 +308,8 @@ describe("CloudflareAIGatewayPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
-            api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("openai/gpt-5"),
+            package: "aisdk:test-provider",
           }),
           package: "ai-gateway-provider",
           options: { name: "cloudflare-ai-gateway" },
@@ -325,7 +332,8 @@ describe("CloudflareAIGatewayPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
-            api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("openai/gpt-5"),
+            package: "aisdk:test-provider",
           }),
           package: "ai-gateway-provider",
           options: { name: "cloudflare-ai-gateway" },
@@ -354,7 +362,8 @@ describe("CloudflareAIGatewayPlugin", () => {
           const result = yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
-              api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+              modelID: ModelV2.ID.make("openai/gpt-5"),
+              package: "aisdk:test-provider",
             }),
             package: "ai-gateway-provider",
             options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
@@ -380,11 +389,8 @@ describe("CloudflareAIGatewayPlugin", () => {
               ProviderV2.ID.make("cloudflare-ai-gateway"),
               ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
             ),
-            api: {
-              id: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
-              type: "aisdk",
-              package: "test-provider",
-            },
+            modelID: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
+            package: "aisdk:test-provider",
           }),
           package: "ai-gateway-provider",
           options: { name: "cloudflare-ai-gateway" },
@@ -412,7 +418,8 @@ describe("CloudflareAIGatewayPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
-            api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("openai/gpt-5"),
+            package: "aisdk:test-provider",
           }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "cloudflare-ai-gateway" },

+ 31 - 42
packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts

@@ -87,7 +87,7 @@ describe("CloudflareWorkersAIPlugin", () => {
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
-            provider.api = { type: "aisdk", package: "test-provider" }
+            provider.package = ProviderV2.aisdk("test-provider")
           }),
         )
         yield* addPlugin()
@@ -95,15 +95,16 @@ describe("CloudflareWorkersAIPlugin", () => {
         const sdk = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
-            api: { id: ModelV2.ID.make("@cf/model"), ...provider.api },
+            modelID: ModelV2.ID.make("@cf/model"),
+            package: provider.package,
+            settings: provider.settings,
           }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
         })
-        expect(provider.api).toEqual({
-          type: "aisdk",
-          package: "test-provider",
-          url: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1",
+        expect(provider).toMatchObject({
+          package: "aisdk:test-provider",
+          settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1" },
         })
         expect(sdk.sdk).toBeDefined()
       }),
@@ -116,14 +117,14 @@ describe("CloudflareWorkersAIPlugin", () => {
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
-            provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
+            provider.package = ProviderV2.aisdk("test-provider")
+            provider.settings = { ...provider.settings, baseURL: "https://proxy.example/v1" }
           }),
         )
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
-          type: "aisdk",
-          package: "test-provider",
-          url: "https://proxy.example/v1",
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
+          package: "aisdk:test-provider",
+          settings: { baseURL: "https://proxy.example/v1" },
         })
       }),
     ),
@@ -138,12 +139,9 @@ describe("CloudflareWorkersAIPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
-            api: {
-              id: ModelV2.ID.make("@cf/model"),
-              type: "aisdk",
-              package: "@ai-sdk/openai-compatible",
-              url: "https://proxy.example/v1",
-            },
+            modelID: ModelV2.ID.make("@cf/model"),
+            package: "aisdk:@ai-sdk/openai-compatible",
+            settings: { baseURL: "https://proxy.example/v1" },
           }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
@@ -159,15 +157,14 @@ describe("CloudflareWorkersAIPlugin", () => {
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
-            provider.api = { type: "aisdk", package: "test-provider" }
-            provider.request.body.accountId = "configured-acct"
+            provider.package = ProviderV2.aisdk("test-provider")
+            provider.settings = { ...provider.settings, accountId: "configured-acct" }
           }),
         )
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
-          type: "aisdk",
-          package: "test-provider",
-          url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
+          package: "aisdk:test-provider",
+          settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" },
         })
       }),
     ),
@@ -182,12 +179,9 @@ describe("CloudflareWorkersAIPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
-            api: {
-              id: ModelV2.ID.make("@cf/model"),
-              type: "aisdk",
-              package: "@ai-sdk/openai-compatible",
-              url: "https://proxy.example/v1",
-            },
+            modelID: ModelV2.ID.make("@cf/model"),
+            package: "aisdk:@ai-sdk/openai-compatible",
+            settings: { baseURL: "https://proxy.example/v1" },
           }),
           package: "@ai-sdk/openai-compatible",
           options: {
@@ -214,12 +208,9 @@ describe("CloudflareWorkersAIPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
-            api: {
-              id: ModelV2.ID.make("@cf/model"),
-              type: "aisdk",
-              package: "@ai-sdk/openai-compatible",
-              url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
-            },
+            modelID: ModelV2.ID.make("@cf/model"),
+            package: "aisdk:@ai-sdk/openai-compatible",
+            settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" },
           }),
           package: "@ai-sdk/openai-compatible",
           options: {
@@ -243,7 +234,8 @@ describe("CloudflareWorkersAIPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
-          api: { id: ModelV2.ID.make("@cf/api-model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("@cf/api-model"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -262,12 +254,9 @@ describe("CloudflareWorkersAIPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
-            api: {
-              id: ModelV2.ID.make("@cf/model"),
-              type: "aisdk",
-              package: "@ai-sdk/anthropic",
-              url: "https://proxy.example/v1",
-            },
+            modelID: ModelV2.ID.make("@cf/model"),
+            package: "aisdk:@ai-sdk/anthropic",
+            settings: { baseURL: "https://proxy.example/v1" },
           }),
           package: "@ai-sdk/anthropic",
           options: { name: "cloudflare-workers-ai" },

+ 8 - 4
packages/core/test/plugin/provider-cohere.test.ts

@@ -56,7 +56,8 @@ describe("CoherePlugin", () => {
       const ignored = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
-          api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("command"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "cohere" },
@@ -66,7 +67,8 @@ describe("CoherePlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
-          api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("command"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/cohere",
         options: { name: "cohere" },
@@ -83,7 +85,8 @@ describe("CoherePlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")),
-          api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
+          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" },
@@ -108,7 +111,8 @@ describe("CoherePlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")),
-          api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("command-r-plus"),
+          package: "aisdk:test-provider",
         }),
         sdk,
         options: {},

+ 13 - 11
packages/core/test/plugin/provider-deepinfra.test.ts

@@ -48,7 +48,8 @@ describe("DeepInfraPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:@ai-sdk/deepinfra",
         }),
         package: "@ai-sdk/deepinfra",
         options: { name: "deepinfra" },
@@ -66,7 +67,8 @@ describe("DeepInfraPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:@ai-sdk/deepinfra",
         }),
         package: "@ai-sdk/deepinfra",
         options: { name: "custom-deepinfra", apiKey: "test" },
@@ -85,7 +87,8 @@ describe("DeepInfraPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:@ai-sdk/deepinfra",
         }),
         package: "@ai-sdk/deepinfra",
         options: { name: "deepinfra", apiKey: "test" },
@@ -111,7 +114,8 @@ describe("DeepInfraPlugin", () => {
           const ignored = yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
-              api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
+              modelID: ModelV2.ID.make("model"),
+              package: "aisdk:@ai-sdk/deepinfra",
             }),
             package: item,
             options: { name: "deepinfra" },
@@ -122,7 +126,8 @@ describe("DeepInfraPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:@ai-sdk/deepinfra",
         }),
         package: "@ai-sdk/deepinfra",
         options: { name: "deepinfra" },
@@ -141,17 +146,14 @@ describe("DeepInfraPlugin", () => {
       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")),
-          api: {
-            id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
-            type: "aisdk",
-            package: "@ai-sdk/deepinfra",
-          },
+          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.api.id)
+      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"])
     }),

+ 17 - 9
packages/core/test/plugin/provider-dynamic.test.ts

@@ -54,7 +54,8 @@ describe("DynamicProviderPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
-          api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
+          modelID: ModelV2.ID.make("test-model"),
+          package: ProviderV2.aisdk(fixtureProvider),
         }),
         package: fixtureProvider,
         options: { name: "custom", marker: "dynamic" },
@@ -72,7 +73,8 @@ describe("DynamicProviderPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
-          api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
+          modelID: ModelV2.ID.make("test-model"),
+          package: ProviderV2.aisdk(fixtureProvider),
         }),
         package: fixtureProvider,
         options: { name: "custom", marker: "dynamic" },
@@ -89,7 +91,8 @@ describe("DynamicProviderPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
-          api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
+          modelID: ModelV2.ID.make("test-model"),
+          package: ProviderV2.aisdk(fixtureProvider),
         }),
         package: fixtureProvider,
         options: { name: "custom-provider", marker: "dynamic" },
@@ -105,7 +108,8 @@ describe("DynamicProviderPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
-          api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: "fixture-provider" },
+          modelID: ModelV2.ID.make("test-model"),
+          package: "aisdk:fixture-provider",
         }),
         package: "fixture-provider",
         options: { name: "npm-provider", marker: "npm" },
@@ -122,7 +126,8 @@ describe("DynamicProviderPlugin", () => {
         .language(
           ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")),
-            api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
+            modelID: ModelV2.ID.make("alias"),
+            package: "aisdk:fixture-provider",
           }),
         )
         .pipe(Effect.exit)
@@ -139,7 +144,8 @@ describe("DynamicProviderPlugin", () => {
         .language(
           ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")),
-            api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "file:///missing/provider-factory.js" },
+            modelID: ModelV2.ID.make("alias"),
+            package: "aisdk:file:///missing/provider-factory.js",
           }),
         )
         .pipe(Effect.exit)
@@ -158,7 +164,8 @@ describe("DynamicProviderPlugin", () => {
         .language(
           ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")),
-            api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
+            modelID: ModelV2.ID.make("alias"),
+            package: "aisdk:fixture-provider",
           }),
         )
         .pipe(Effect.exit)
@@ -167,7 +174,7 @@ describe("DynamicProviderPlugin", () => {
     }),
   )
 
-  itWithAISDK.effect("uses the model api.id for the default language model", () =>
+  itWithAISDK.effect("uses the model modelID for the default language model", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
@@ -175,7 +182,8 @@ describe("DynamicProviderPlugin", () => {
       const language = yield* aisdk.language(
         ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")),
-          api: { id: ModelV2.ID.make("test-model-api"), type: "aisdk", package: fixtureProvider },
+          modelID: ModelV2.ID.make("test-model-api"),
+          package: ProviderV2.aisdk(fixtureProvider),
         }),
       )
       expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } })

+ 8 - 8
packages/core/test/plugin/provider-gateway.test.ts

@@ -45,7 +45,8 @@ describe("GatewayPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/gateway",
         options: { name: "gateway" },
@@ -65,11 +66,8 @@ describe("GatewayPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")),
-          api: {
-            id: ModelV2.ID.make("anthropic/claude-sonnet-4"),
-            type: "aisdk",
-            package: "test-provider",
-          },
+          modelID: ModelV2.ID.make("anthropic/claude-sonnet-4"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/gateway",
         options: { name: "vercel", apiKey: "test-key" },
@@ -91,7 +89,8 @@ describe("GatewayPlugin", () => {
         const ignored = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
-            api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make(modelID),
+            package: "aisdk:test-provider",
           }),
           package: "@ai-sdk/vercel",
           options: { name: "vercel" },
@@ -101,7 +100,8 @@ describe("GatewayPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
-            api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make(modelID),
+            package: "aisdk:test-provider",
           }),
           package: "@ai-sdk/gateway",
           options: { name: "vercel" },

+ 26 - 13
packages/core/test/plugin/provider-github-copilot.test.ts

@@ -47,7 +47,8 @@ describe("GithubCopilotPlugin", () => {
       const ignored = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
-          api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "github-copilot" },
@@ -55,7 +56,8 @@ describe("GithubCopilotPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
-          api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/github-copilot",
         options: { name: "github-copilot" },
@@ -74,7 +76,8 @@ describe("GithubCopilotPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
-          api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("claude-sonnet-4"),
+          package: "aisdk:test-provider",
         }),
         sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
         options: {},
@@ -92,7 +95,8 @@ describe("GithubCopilotPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
-          api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("claude-sonnet-4"),
+          package: "aisdk:test-provider",
         }),
         sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
         options: {},
@@ -110,7 +114,8 @@ describe("GithubCopilotPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
-          api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -118,7 +123,8 @@ describe("GithubCopilotPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
-          api: { id: ModelV2.ID.make("gpt-5.1-codex"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5.1-codex"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -126,7 +132,8 @@ describe("GithubCopilotPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
-          api: { id: ModelV2.ID.make("gpt-4o"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-4o"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -134,7 +141,8 @@ describe("GithubCopilotPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
-          api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5-mini"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -142,7 +150,8 @@ describe("GithubCopilotPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")),
-          api: { id: ModelV2.ID.make("gpt-5-mini-2025-08-07"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -166,7 +175,8 @@ describe("GithubCopilotPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
-          api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -174,7 +184,8 @@ describe("GithubCopilotPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
-          api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5-mini"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -182,7 +193,8 @@ describe("GithubCopilotPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
-          api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("claude-sonnet-4"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -230,7 +242,8 @@ describe("GithubCopilotPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
-          api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},

+ 23 - 18
packages/core/test/plugin/provider-gitlab.test.ts

@@ -70,7 +70,8 @@ describe("GitLabPlugin", () => {
           yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
-              api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
+              modelID: ModelV2.ID.make("claude"),
+              package: "aisdk:test-provider",
             }),
             package: "gitlab-ai-provider",
             options: { name: "gitlab" },
@@ -107,7 +108,8 @@ describe("GitLabPlugin", () => {
           yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
-              api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
+              modelID: ModelV2.ID.make("claude"),
+              package: "aisdk:test-provider",
             }),
             package: "gitlab-ai-provider",
             options: { name: "gitlab" },
@@ -132,7 +134,8 @@ describe("GitLabPlugin", () => {
           yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
-              api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
+              modelID: ModelV2.ID.make("claude"),
+              package: "aisdk:test-provider",
             }),
             package: "gitlab-ai-provider",
             options: {
@@ -173,7 +176,8 @@ describe("GitLabPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
-          api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("claude"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai",
         options: { name: "gitlab" },
@@ -192,11 +196,10 @@ describe("GitLabPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
-          api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
-          request: {
-            headers: {},
-            body: { workflowRef: "ref", workflowDefinition: "definition" },
-          },
+          modelID: ModelV2.ID.make("duo-workflow-custom"),
+          package: "aisdk:test-provider",
+          headers: {},
+          settings: { workflowRef: "ref", workflowDefinition: "definition" },
         }),
         sdk: {
           workflowChat: (id: string, options: unknown) => {
@@ -227,7 +230,8 @@ describe("GitLabPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
-          api: { id: ModelV2.ID.make("duo-workflow-exact"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("duo-workflow-exact"),
+          package: "aisdk:test-provider",
         }),
         sdk: {
           workflowChat: (id: string, options: unknown) => {
@@ -245,7 +249,7 @@ describe("GitLabPlugin", () => {
     }),
   )
 
-  it.effect("uses provider feature flags instead of request feature flags", () =>
+  it.effect("uses provider feature flags instead of model settings feature flags", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
@@ -254,11 +258,10 @@ describe("GitLabPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
-          api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
-          request: {
-            headers: {},
-            body: { featureFlags: { request_flag: true } },
-          },
+          modelID: ModelV2.ID.make("duo-workflow-custom"),
+          package: "aisdk:test-provider",
+          headers: {},
+          settings: { featureFlags: { request_flag: true } },
         }),
         sdk: {
           workflowChat: (id: string, options: unknown) => {
@@ -282,8 +285,10 @@ describe("GitLabPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
-          api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
-          request: { headers: { h: "v" }, body: {} },
+          modelID: ModelV2.ID.make("claude"),
+          package: "aisdk:test-provider",
+          headers: { h: "v" },
+          settings: {},
         }),
         sdk: {
           workflowChat: () => undefined,

+ 29 - 22
packages/core/test/plugin/provider-google-vertex-anthropic.test.ts

@@ -64,16 +64,16 @@ describe("GoogleVertexAnthropicPlugin", () => {
           const catalog = yield* Catalog.Service
           yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
-              provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
+              provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic")
             }),
           )
           yield* addPlugin(GoogleVertexAnthropicPlugin)
-          expect(
-            (yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project,
-          ).toBe("cloud-project")
-          expect(
-            (yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
-          ).toBe("cloud-location")
+          expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe(
+            "cloud-project",
+          )
+          expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
+            "cloud-location",
+          )
         }),
     ),
   )
@@ -84,18 +84,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
-            provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
-            provider.request.body.project = "configured-project"
-            provider.request.body.location = "configured-location"
+            provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic")
+            provider.settings = { ...provider.settings, project: "configured-project", location: "configured-location" }
           }),
         )
         yield* addPlugin(GoogleVertexAnthropicPlugin)
-        expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project).toBe(
+        expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe(
           "configured-project",
         )
-        expect(
-          (yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
-        ).toBe("configured-location")
+        expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
+          "configured-location",
+        )
       }),
     ),
   )
@@ -121,7 +120,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
                 ProviderV2.ID.make("google-vertex-anthropic"),
                 ModelV2.ID.make("claude-sonnet-4-5"),
               ),
-              api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+              modelID: ModelV2.ID.make("claude-sonnet-4-5"),
+              package: "aisdk:test-provider",
             }),
             package: "@ai-sdk/google-vertex/anthropic",
             options: { name: "google-vertex-anthropic" },
@@ -147,7 +147,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
                 ProviderV2.ID.make("google-vertex-anthropic"),
                 ModelV2.ID.make("claude-sonnet-4-5"),
               ),
-              api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+              modelID: ModelV2.ID.make("claude-sonnet-4-5"),
+              package: "aisdk:test-provider",
             }),
             package: "@ai-sdk/google-vertex/anthropic",
             options: { name: "google-vertex-anthropic" },
@@ -167,7 +168,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
-          api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("claude-sonnet-4-5"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/google-vertex/anthropic",
         options: { name: "google-vertex", project: "project", location: "eu" },
@@ -186,7 +188,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
-          api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("claude-sonnet-4-5"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/google-vertex/anthropic",
         options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
@@ -204,7 +207,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
       const sdkResult = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
-          api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/google-vertex/anthropic",
         options: { name: "google-vertex", project: "project", location: "us" },
@@ -212,7 +216,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
       const languageResult = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
-          api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
+          package: "aisdk:test-provider",
         }),
         sdk: sdkResult.sdk,
         options: {},
@@ -234,7 +239,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
-          api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
+          package: "aisdk:test-provider",
         }),
         sdk: { languageModel: selector(calls) },
         options: {},
@@ -252,7 +258,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
-          api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("claude-sonnet-4-5"),
+          package: "aisdk:test-provider",
         }),
         sdk: { languageModel: selector(calls) },
         options: {},

+ 64 - 71
packages/core/test/plugin/provider-google-vertex.test.ts

@@ -92,17 +92,14 @@ describe("GoogleVertexPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) =>
         catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://opencode.ai/zen/v1",
-          }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { ...provider.settings, baseURL: "https://opencode.ai/zen/v1" }
         }),
       )
       yield* addPlugin()
 
       const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode))
-      expect(provider.request.body).toEqual({})
+      expect(provider.settings).toEqual({ baseURL: "https://opencode.ai/zen/v1" })
     }),
   )
 
@@ -121,21 +118,24 @@ describe("GoogleVertexPlugin", () => {
           const catalog = yield* Catalog.Service
           yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
-              provider.api = {
-                type: "aisdk",
-                package: "@ai-sdk/openai-compatible",
-                url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
+              provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+              provider.settings = {
+                ...provider.settings,
+                baseURL:
+                  "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
               }
             }),
           )
           yield* addPlugin()
           const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
-          expect(provider.request.body.project).toBe("google-cloud-project")
-          expect(provider.request.body.location).toBe("google-vertex-location")
-          expect(provider.api).toEqual({
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
+          expect(provider.settings?.project).toBe("google-cloud-project")
+          expect(provider.settings?.location).toBe("google-vertex-location")
+          expect(provider).toMatchObject({
+            package: "aisdk:@ai-sdk/openai-compatible",
+            settings: {
+              baseURL:
+                "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
+            },
           })
         }),
     ),
@@ -160,10 +160,11 @@ describe("GoogleVertexPlugin", () => {
           const catalog = yield* Catalog.Service
           yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
-              provider.api = {
-                type: "aisdk",
-                package: "@ai-sdk/openai-compatible",
-                url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
+              provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+              provider.settings = {
+                ...provider.settings,
+                baseURL:
+                  "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
               }
             }),
           )
@@ -172,21 +173,20 @@ describe("GoogleVertexPlugin", () => {
           yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
-              api: {
-                id: ModelV2.ID.make("gemini"),
-                type: "aisdk",
-                package: "@ai-sdk/google-vertex",
-              },
+              modelID: ModelV2.ID.make("gemini"),
+              package: "aisdk:@ai-sdk/google-vertex",
             }),
             package: "@ai-sdk/google-vertex",
             options: { name: "google-vertex" },
           })
 
-          expect(provider.request.body.project).toBe("vertex-project")
-          expect(provider.api).toEqual({
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
+          expect(provider.settings?.project).toBe("vertex-project")
+          expect(provider).toMatchObject({
+            package: "aisdk:@ai-sdk/openai-compatible",
+            settings: {
+              baseURL:
+                "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
+            },
           })
           expect(vertexOptions[0].project).toBe("vertex-project")
           expect(vertexOptions[0].location).toBe("europe-west4")
@@ -209,23 +209,22 @@ describe("GoogleVertexPlugin", () => {
           const catalog = yield* Catalog.Service
           yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
-              provider.api = {
-                type: "aisdk",
-                package: "@ai-sdk/openai-compatible",
-                url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
+              provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+              provider.settings = {
+                ...provider.settings,
+                baseURL:
+                  "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
               }
-              provider.request.body.project = "config-project"
-              provider.request.body.location = "global"
+              provider.settings = { ...provider.settings, project: "config-project", location: "global" }
             }),
           )
           yield* addPlugin()
           const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
-          expect(provider.request.body.project).toBe("config-project")
-          expect(provider.request.body.location).toBe("global")
-          expect(provider.api).toEqual({
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global",
+          expect(provider.settings?.project).toBe("config-project")
+          expect(provider.settings?.location).toBe("global")
+          expect(provider).toMatchObject({
+            package: "aisdk:@ai-sdk/openai-compatible",
+            settings: { baseURL: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global" },
           })
         }),
     ),
@@ -236,21 +235,20 @@ describe("GoogleVertexPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) =>
         catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = {
+            ...provider.settings,
+            baseURL:
+              "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
           }
-          provider.request.body.project = "config-project"
-          provider.request.body.location = "eu"
+          provider.settings = { ...provider.settings, project: "config-project", location: "eu" }
         }),
       )
       yield* addPlugin()
       const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
-      expect(provider.api).toEqual({
-        type: "aisdk",
-        package: "@ai-sdk/openai-compatible",
-        url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu",
+      expect(provider).toMatchObject({
+        package: "aisdk:@ai-sdk/openai-compatible",
+        settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" },
       })
     }),
   )
@@ -270,14 +268,14 @@ describe("GoogleVertexPlugin", () => {
           const catalog = yield* Catalog.Service
           yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
-              provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" }
-              provider.request.body.project = "config-project"
+              provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex")
+              provider.settings = { ...provider.settings, project: "config-project" }
             }),
           )
           yield* addPlugin()
           const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
-          expect(provider.request.body.project).toBe("config-project")
-          expect(provider.request.body.location).toBe("us-central1")
+          expect(provider.settings?.project).toBe("config-project")
+          expect(provider.settings?.location).toBe("us-central1")
         }),
     ),
   )
@@ -297,11 +295,8 @@ describe("GoogleVertexPlugin", () => {
           yield* aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
-              api: {
-                id: ModelV2.ID.make("gemini"),
-                type: "aisdk",
-                package: "@ai-sdk/google-vertex",
-              },
+              modelID: ModelV2.ID.make("gemini"),
+              package: "aisdk:@ai-sdk/google-vertex",
             }),
             package: "@ai-sdk/google-vertex",
             options: { name: "google-vertex" },
@@ -345,11 +340,8 @@ describe("GoogleVertexPlugin", () => {
           aisdk.runSDK({
             model: ModelV2.Info.make({
               ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
-              api: {
-                id: ModelV2.ID.make("gemini"),
-                type: "aisdk",
-                package: "@ai-sdk/openai-compatible",
-              },
+              modelID: ModelV2.ID.make("gemini"),
+              package: "aisdk:@ai-sdk/openai-compatible",
             }),
             package: "@ai-sdk/openai-compatible",
             options: { name: "google-vertex" },
@@ -359,11 +351,11 @@ describe("GoogleVertexPlugin", () => {
             ;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = originalFetch
           }),
       )
-      expect(fetchCalls).toHaveLength(1)
+      const vertexCalls = fetchCalls.filter((call) => call.input === "https://vertex.example")
+      expect(vertexCalls).toHaveLength(1)
       expect(googleAuthOptions).toEqual([{ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }])
-      expect(fetchCalls[0].input).toBe("https://vertex.example")
-      expect(new Headers(fetchCalls[0].init?.headers).get("authorization")).toBe("Bearer vertex-token")
-      expect(new Headers(fetchCalls[0].init?.headers).get("x-test")).toBe("1")
+      expect(new Headers(vertexCalls[0].init?.headers).get("authorization")).toBe("Bearer vertex-token")
+      expect(new Headers(vertexCalls[0].init?.headers).get("x-test")).toBe("1")
     }),
   )
 
@@ -376,7 +368,8 @@ describe("GoogleVertexPlugin", () => {
       yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
-          api: { id: ModelV2.ID.make(" gemini-2.5-pro "), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make(" gemini-2.5-pro "),
+          package: "aisdk:test-provider",
         }),
         sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
         options: {},

+ 27 - 4
packages/core/test/plugin/provider-google.test.ts

@@ -27,7 +27,8 @@ describe("GooglePlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
-          api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
+          modelID: ModelV2.ID.make("gemini"),
+          package: "aisdk:@ai-sdk/google",
         }),
         package: "@ai-sdk/google",
         options: { name: "custom-google", apiKey: "test" },
@@ -45,7 +46,8 @@ describe("GooglePlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
-          api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
+          modelID: ModelV2.ID.make("gemini"),
+          package: "aisdk:@ai-sdk/google",
         }),
         package: "@ai-sdk/google-vertex",
         options: { name: "google" },
@@ -62,7 +64,8 @@ describe("GooglePlugin", () => {
       const sdkEvent = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
-          api: { id: ModelV2.ID.make("gemini-api"), type: "aisdk", package: "@ai-sdk/google" },
+          modelID: ModelV2.ID.make("gemini-api"),
+          package: "aisdk:@ai-sdk/google",
         }),
         package: "@ai-sdk/google",
         options: { name: "custom-google", apiKey: "test" },
@@ -72,9 +75,29 @@ describe("GooglePlugin", () => {
         sdk: sdkEvent.sdk,
         options: sdkEvent.options,
       })
-      const language = result.language ?? result.sdk.languageModel(result.model.api.id)
+      const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id)
       expect(language.modelId).toBe("gemini-api")
       expect(language.provider).toBe("custom-google")
     }),
   )
+
+  it.effect("wraps AI SDK language models for the native runner", () =>
+    Effect.gen(function* () {
+      const aisdk = yield* AISDK.Service
+      yield* addPlugin()
+
+      const resolved = yield* aisdk.model(
+        ModelV2.Info.make({
+          ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
+          modelID: ModelV2.ID.make("gemini-api"),
+          package: "aisdk:@ai-sdk/google",
+          settings: { apiKey: "test" },
+        }),
+      )
+
+      expect(String(resolved.id)).toBe("gemini-api")
+      expect(String(resolved.provider)).toBe("custom-google")
+      expect(resolved.route.id).toBe("ai-sdk:@ai-sdk/google")
+    }),
+  )
 })

+ 12 - 11
packages/core/test/plugin/provider-groq.test.ts

@@ -28,7 +28,8 @@ describe("GroqPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
-          api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
+          modelID: ModelV2.ID.make("llama"),
+          package: "aisdk:@ai-sdk/groq",
         }),
         package: "@ai-sdk/groq",
         options: { name: "groq" },
@@ -45,7 +46,8 @@ describe("GroqPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
-          api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
+          modelID: ModelV2.ID.make("llama"),
+          package: "aisdk:@ai-sdk/groq",
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "groq" },
@@ -62,7 +64,8 @@ describe("GroqPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
-          api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
+          modelID: ModelV2.ID.make("llama"),
+          package: "aisdk:@ai-sdk/groq",
         }),
         package: "@ai-sdk/groq/compat",
         options: { name: "groq" },
@@ -79,7 +82,8 @@ describe("GroqPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")),
-          api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
+          modelID: ModelV2.ID.make("llama"),
+          package: "aisdk:@ai-sdk/groq",
         }),
         package: "@ai-sdk/groq",
         options: { name: "custom-groq", apiKey: "test" },
@@ -93,7 +97,7 @@ describe("GroqPlugin", () => {
     }),
   )
 
-  it.effect("uses the default languageModel(api.id) behavior", () =>
+  it.effect("uses the default languageModel(modelID) behavior", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
@@ -104,16 +108,13 @@ describe("GroqPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
-          api: {
-            id: ModelV2.ID.make("llama-api"),
-            type: "aisdk",
-            package: "@ai-sdk/groq",
-          },
+          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.api.id)
+      const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
       expect(language.modelId).toBe("llama-api")
       expect(language.provider).toBe("groq.chat")
     }),

+ 18 - 29
packages/core/test/plugin/provider-kilo.test.ts

@@ -22,27 +22,24 @@ describe("KiloPlugin", () => {
     Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.kilo")),
   )
 
-  it.effect("applies legacy referer headers only to kilo", () =>
+  it.effect("applies legacy referer headers only to Kilo endpoints", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://api.kilo.ai/api/gateway",
-          }
-          provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
+          provider.headers = { Existing: "value" }
         })
         catalog.provider.update(ProviderV2.ID.openrouter, () => {})
       })
       yield* addPlugin()
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
     }),
   )
 
@@ -51,49 +48,41 @@ describe("KiloPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://api.kilo.ai/api/gateway",
-          }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
         })
       })
       yield* addPlugin()
 
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty(
-        "http-referer",
-      )
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("x-title")
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("X-Source")
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("http-referer")
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("x-title")
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("X-Source")
     }),
   )
 
-  it.effect("uses the legacy provider-id guard instead of endpoint package matching", () =>
+  it.effect("uses endpoint package matching instead of a provider ID guard", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://api.kilo.ai/api/gateway",
-          }
+          provider.package = ProviderV2.aisdk("kilo")
         })
         catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => {
-          provider.api = { type: "aisdk", package: "kilo" }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
         })
       })
       yield* addPlugin()
 
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toBeUndefined()
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.headers).toEqual({
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.request.headers).toEqual({})
     }),
   )
 })

+ 8 - 14
packages/core/test/plugin/provider-llmgateway.test.ts

@@ -34,23 +34,20 @@ describe("LLMGatewayPlugin", () => {
       })
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://api.llmgateway.io/v1",
-          }
-          provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
+          provider.headers = { Existing: "value" }
         })
         catalog.provider.update(ProviderV2.ID.openrouter, () => {})
       })
       yield* addPlugin()
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toEqual({
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-Source": "opencode",
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
     }),
   )
 
@@ -64,17 +61,14 @@ describe("LLMGatewayPlugin", () => {
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
           provider.disabled = true
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://api.llmgateway.io/v1",
-          }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
         })
       })
       yield* addPlugin()
 
       expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.disabled).toBe(true)
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toBeUndefined()
     }),
   )
 })

+ 12 - 7
packages/core/test/plugin/provider-mistral.test.ts

@@ -28,7 +28,8 @@ describe("MistralPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
-          api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("mistral-large"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/mistral",
         options: { name: "mistral" },
@@ -45,7 +46,8 @@ describe("MistralPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
-          api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("mistral-large"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "mistral" },
@@ -68,7 +70,8 @@ describe("MistralPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
-          api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("mistral-large"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/mistral",
         options: { name: "mistral" },
@@ -92,7 +95,8 @@ describe("MistralPlugin", () => {
       yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")),
-          api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("mistral-large"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/mistral",
         options: { name: "custom-mistral" },
@@ -101,7 +105,7 @@ describe("MistralPlugin", () => {
     }),
   )
 
-  it.effect("leaves Mistral language selection on the default sdk.languageModel(api.id) path", () =>
+  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
@@ -116,12 +120,13 @@ describe("MistralPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")),
-          api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("mistral-large"),
+          package: "aisdk:test-provider",
         }),
         sdk,
         options: {},
       })
-      const language = result.language ?? sdk.languageModel(result.model.api.id)
+      const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
       expect(calls).toEqual(["languageModel:mistral-large"])
       expect(language).toBeDefined()
     }),

+ 12 - 25
packages/core/test/plugin/provider-nvidia.test.ts

@@ -27,23 +27,20 @@ describe("NvidiaPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://integrate.api.nvidia.com/v1",
-          }
-          provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
+          provider.headers = { Existing: "value" }
         })
         catalog.provider.update(ProviderV2.ID.openrouter, () => {})
       })
       yield* addPlugin()
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-BILLING-INVOKE-ORIGIN": "OpenCode",
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
     }),
   )
 
@@ -52,16 +49,13 @@ describe("NvidiaPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://integrate.api.nvidia.com/v1",
-          }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
         })
       })
       yield* addPlugin()
 
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-BILLING-INVOKE-ORIGIN": "OpenCode",
@@ -74,21 +68,14 @@ describe("NvidiaPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://integrate.api.nvidia.com/v1",
-          }
-          provider.request = {
-            settings: {},
-            headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" },
-            body: { baseURL: "https://integrate.api.nvidia.com/v1" },
-          }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
+          provider.headers = { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" }
         })
       })
       yield* addPlugin()
 
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-BILLING-INVOKE-ORIGIN": "CustomOrigin",

+ 10 - 5
packages/core/test/plugin/provider-openai-compatible.test.ts

@@ -27,7 +27,8 @@ describe("OpenAICompatiblePlugin", () => {
       const defaulted = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "custom" },
@@ -35,7 +36,8 @@ describe("OpenAICompatiblePlugin", () => {
       const disabled = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "custom", includeUsage: false },
@@ -53,7 +55,8 @@ describe("OpenAICompatiblePlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "file:///tmp/@ai-sdk/openai-compatible-provider.js",
         options: { name: "custom" },
@@ -76,7 +79,8 @@ describe("OpenAICompatiblePlugin", () => {
       yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "custom-provider", baseURL: "https://example.com/v1" },
@@ -96,7 +100,8 @@ describe("OpenAICompatiblePlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "cloudflare-workers-ai" },

+ 16 - 12
packages/core/test/plugin/provider-openai.test.ts

@@ -82,7 +82,8 @@ describe("OpenAIPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
-          api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         package: "@ai-sdk/openai",
         options: { name: "custom-openai", apiKey: "test" },
@@ -99,7 +100,8 @@ describe("OpenAIPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
-          api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "openai" },
@@ -117,7 +119,8 @@ describe("OpenAIPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
-          api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -136,7 +139,8 @@ describe("OpenAIPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
-          api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -152,10 +156,10 @@ describe("OpenAIPlugin", () => {
       yield* catalog.transform((catalog) => {
         const item = ProviderV2.Info.make({
           ...ProviderV2.Info.empty(ProviderV2.ID.openai),
-          api: { type: "aisdk", package: "@ai-sdk/openai" },
+          package: ProviderV2.aisdk("@ai-sdk/openai"),
         })
         catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
+          draft.package = item.package
         })
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
@@ -175,10 +179,10 @@ describe("OpenAIPlugin", () => {
       yield* catalog.transform((catalog) => {
         const item = ProviderV2.Info.make({
           ...ProviderV2.Info.empty(ProviderV2.ID.openai),
-          api: { type: "aisdk", package: "@ai-sdk/openai" },
+          package: ProviderV2.aisdk("@ai-sdk/openai"),
         })
         catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
+          draft.package = item.package
         })
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), (model) => {
           model.cost = [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }]
@@ -220,10 +224,10 @@ describe("OpenAIPlugin", () => {
       yield* catalog.transform((catalog) => {
         const item = ProviderV2.Info.make({
           ...ProviderV2.Info.empty(ProviderV2.ID.openai),
-          api: { type: "aisdk", package: "@ai-sdk/openai" },
+          package: ProviderV2.aisdk("@ai-sdk/openai"),
         })
         catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
+          draft.package = item.package
         })
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), () => {})
         catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {})
@@ -248,10 +252,10 @@ describe("OpenAIPlugin", () => {
       yield* catalog.transform((catalog) => {
         const item = ProviderV2.Info.make({
           ...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")),
-          api: { type: "aisdk", package: "test-provider" },
+          package: ProviderV2.aisdk("test-provider"),
         })
         catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
+          draft.package = item.package
         })
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
       })

+ 38 - 36
packages/core/test/plugin/provider-opencode.test.ts

@@ -172,13 +172,10 @@ describe("OpencodePlugin", () => {
           expect(provider).toMatchObject({
             name: "Remote",
             integrationID: "opencode",
-            api: {
-              type: "aisdk",
-              package: "@ai-sdk/openai-compatible",
-              url: `${server.url.origin}/v1`,
-            },
+            package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
+            settings: { baseURL: `${server.url.origin}/v1`, custom: "value" },
+            headers: { "x-org-id": "org" },
           })
-          expect(provider.request).toEqual({ settings: {}, headers: { "x-org-id": "org" }, body: { custom: "value" } })
           expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined()
 
           const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model")))
@@ -188,8 +185,10 @@ describe("OpencodePlugin", () => {
             capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
             cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
             limit: { context: 1000, output: 100 },
+            package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
+            settings: { baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 },
+            headers: { "x-org-id": "org" },
           })
-          expect(model.request.body).toEqual({ custom: "value", temperature: 0.5 })
           expect(model.variants).toEqual([
             {
               id: ModelV2.VariantID.make("custom"),
@@ -199,9 +198,8 @@ describe("OpencodePlugin", () => {
             },
             {
               id: ModelV2.VariantID.make("high"),
-              settings: {},
+              settings: { temperature: 0.2 },
               headers: {},
-              body: { temperature: 0.2 },
             },
           ])
           expect(
@@ -221,11 +219,12 @@ describe("OpencodePlugin", () => {
         yield* catalog.transform((catalog) => {
           const provider = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
-            api: { type: "aisdk", package: "test-provider" },
+            package: ProviderV2.aisdk("test-provider"),
           })
           const model = ModelV2.Info.make({
             ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
-            api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("paid"),
+            package: ProviderV2.aisdk("test-provider"),
             cost: cost(1),
           })
           catalog.provider.update(provider.id, () => {})
@@ -234,7 +233,7 @@ describe("OpencodePlugin", () => {
           })
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
       }),
     ),
@@ -247,11 +246,12 @@ describe("OpencodePlugin", () => {
         yield* catalog.transform((catalog) => {
           const provider = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
-            api: { type: "aisdk", package: "test-provider" },
+            package: ProviderV2.aisdk("test-provider"),
           })
           const model = ModelV2.Info.make({
             ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")),
-            api: { id: ModelV2.ID.make("free"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("free"),
+            package: ProviderV2.aisdk("test-provider"),
             cost: cost(0),
           })
           catalog.provider.update(provider.id, () => {})
@@ -260,7 +260,7 @@ describe("OpencodePlugin", () => {
           })
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
       }),
     ),
@@ -273,11 +273,12 @@ describe("OpencodePlugin", () => {
         yield* catalog.transform((catalog) => {
           const provider = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
-            api: { type: "aisdk", package: "test-provider" },
+            package: ProviderV2.aisdk("test-provider"),
           })
           const model = ModelV2.Info.make({
             ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")),
-            api: { id: ModelV2.ID.make("output-only"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("output-only"),
+            package: ProviderV2.aisdk("test-provider"),
             cost: cost(0, 1),
           })
           catalog.provider.update(provider.id, () => {})
@@ -286,7 +287,7 @@ describe("OpencodePlugin", () => {
           })
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(
           true,
         )
@@ -301,11 +302,12 @@ describe("OpencodePlugin", () => {
         yield* catalog.transform((catalog) => {
           const provider = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
-            api: { type: "aisdk", package: "test-provider" },
+            package: ProviderV2.aisdk("test-provider"),
           })
           const model = ModelV2.Info.make({
             ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
-            api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("paid"),
+            package: ProviderV2.aisdk("test-provider"),
             cost: cost(1),
           })
           catalog.provider.update(provider.id, () => {})
@@ -314,7 +316,7 @@ describe("OpencodePlugin", () => {
           })
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined()
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
       }),
     ),
@@ -334,11 +336,12 @@ describe("OpencodePlugin", () => {
         yield* catalog.transform((catalog) => {
           const provider = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
-            api: { type: "aisdk", package: "test-provider" },
+            package: ProviderV2.aisdk("test-provider"),
           })
           const model = ModelV2.Info.make({
             ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
-            api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("paid"),
+            package: ProviderV2.aisdk("test-provider"),
             cost: cost(1),
           })
           catalog.provider.update(provider.id, () => {})
@@ -347,7 +350,7 @@ describe("OpencodePlugin", () => {
           })
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined()
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
       }),
     ),
@@ -360,27 +363,25 @@ describe("OpencodePlugin", () => {
         yield* catalog.transform((catalog) => {
           const provider = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
-            api: { type: "aisdk", package: "test-provider" },
-            request: {
-              settings: {},
-              headers: {},
-              body: { apiKey: "configured" },
-            },
+            package: ProviderV2.aisdk("test-provider"),
+            settings: { apiKey: "configured" },
           })
           const model = ModelV2.Info.make({
             ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
-            api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("paid"),
+            package: ProviderV2.aisdk("test-provider"),
             cost: cost(1),
           })
           catalog.provider.update(provider.id, (draft) => {
-            draft.request = { settings: {}, headers: {}, body: { apiKey: "configured" } }
+            draft.package = provider.package
+            draft.settings = { apiKey: "configured" }
           })
           catalog.model.update(provider.id, model.id, (draft) => {
             draft.cost = [...model.cost]
           })
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("configured")
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("configured")
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
       }),
     ),
@@ -393,11 +394,12 @@ describe("OpencodePlugin", () => {
         yield* catalog.transform((catalog) => {
           const provider = ProviderV2.Info.make({
             ...ProviderV2.Info.empty(ProviderV2.ID.openai),
-            api: { type: "aisdk", package: "test-provider" },
+            package: ProviderV2.aisdk("test-provider"),
           })
           const model = ModelV2.Info.make({
             ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
-            api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("paid"),
+            package: ProviderV2.aisdk("test-provider"),
             cost: cost(1),
           })
           catalog.provider.update(provider.id, () => {})
@@ -406,7 +408,7 @@ describe("OpencodePlugin", () => {
           })
         })
         yield* addPlugin()
-        expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.apiKey).toBeUndefined()
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.apiKey).toBeUndefined()
         expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
       }),
     ),

+ 9 - 7
packages/core/test/plugin/provider-openrouter.test.ts

@@ -30,19 +30,19 @@ describe("OpenRouterPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
-          provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
-          provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
+          provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider")
+          provider.headers = { Existing: "value" }
         })
         catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
       })
       yield* addPlugin()
 
-      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toEqual({
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toBeUndefined()
     }),
   )
 
@@ -55,7 +55,8 @@ describe("OpenRouterPlugin", () => {
       const ignored = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")),
-          api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("openai/gpt-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "openrouter" },
@@ -65,7 +66,8 @@ describe("OpenRouterPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")),
-          api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("openai/gpt-5"),
+          package: ProviderV2.aisdk("test-provider"),
         }),
         package: "@openrouter/ai-sdk-provider",
         options: { name: "custom" },
@@ -79,7 +81,7 @@ describe("OpenRouterPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
-          provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
+          provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider")
         })
         catalog.provider.update(ProviderV2.ID.openai, () => {})
         catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {})

+ 10 - 5
packages/core/test/plugin/provider-perplexity.test.ts

@@ -41,7 +41,8 @@ describe("PerplexityPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
-          api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("sonar"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/perplexity",
         options: { name: "perplexity" },
@@ -58,7 +59,8 @@ describe("PerplexityPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
-          api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("sonar"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/perplexity-compatible",
         options: { name: "perplexity" },
@@ -75,7 +77,8 @@ describe("PerplexityPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
-          api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("sonar"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/perplexity",
         options: { name: "perplexity" },
@@ -92,7 +95,8 @@ describe("PerplexityPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")),
-          api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("sonar"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/perplexity",
         options: { name: "custom-perplexity" },
@@ -110,7 +114,8 @@ describe("PerplexityPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")),
-          api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("sonar"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},

+ 2 - 1
packages/core/test/plugin/provider-sap-ai-core.test.ts

@@ -49,7 +49,8 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
 function model(providerID: string) {
   return ModelV2.Info.make({
     ...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")),
-    api: { id: ModelV2.ID.make("sap-model"), type: "aisdk", package: fixtureProvider },
+    modelID: ModelV2.ID.make("sap-model"),
+    package: ProviderV2.aisdk(fixtureProvider),
   })
 }
 

+ 12 - 6
packages/core/test/plugin/provider-snowflake-cortex.test.ts

@@ -59,7 +59,8 @@ describe("SnowflakeCortexPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")),
-          api: { id: ModelV2.ID.make("gpt-4"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("gpt-4"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai",
         options: { name: "openai" },
@@ -77,7 +78,8 @@ describe("SnowflakeCortexPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
-            api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("claude-sonnet-4-6"),
+            package: "aisdk:test-provider",
           }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -96,7 +98,8 @@ describe("SnowflakeCortexPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
-            api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("claude-sonnet-4-6"),
+            package: "aisdk:test-provider",
           }),
           package: "@ai-sdk/openai-compatible",
           options: {
@@ -119,7 +122,8 @@ describe("SnowflakeCortexPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
-            api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("claude-sonnet-4-6"),
+            package: "aisdk:test-provider",
           }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -138,7 +142,8 @@ describe("SnowflakeCortexPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
-            api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("claude-sonnet-4-6"),
+            package: "aisdk:test-provider",
           }),
           package: "@ai-sdk/openai-compatible",
           options: {
@@ -161,7 +166,8 @@ describe("SnowflakeCortexPlugin", () => {
         const result = yield* aisdk.runSDK({
           model: ModelV2.Info.make({
             ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
-            api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
+            modelID: ModelV2.ID.make("claude-sonnet-4-6"),
+            package: "aisdk:test-provider",
           }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },

+ 13 - 10
packages/core/test/plugin/provider-togetherai.test.ts

@@ -41,7 +41,8 @@ describe("TogetherAIPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/togetherai",
         options: { name: "togetherai" },
@@ -59,7 +60,8 @@ describe("TogetherAIPlugin", () => {
       const ignored = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "file:///tmp/@ai-sdk/togetherai-provider.js",
         options: { name: "togetherai" },
@@ -69,7 +71,8 @@ describe("TogetherAIPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/togetherai",
         options: { name: "togetherai" },
@@ -87,7 +90,8 @@ describe("TogetherAIPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/togetherai",
         options: { name: "custom-togetherai" },
@@ -110,11 +114,8 @@ describe("TogetherAIPlugin", () => {
             ProviderV2.ID.make("togetherai"),
             ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
           ),
-          api: {
-            id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
-            type: "aisdk",
-            package: "test-provider",
-          },
+          modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
+          package: "aisdk:test-provider",
         }),
         sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
         options: {},
@@ -122,7 +123,9 @@ describe("TogetherAIPlugin", () => {
 
       expect(result.language).toBeUndefined()
       expect(calls).toEqual([])
-      expect(result.language ?? fakeSelectorSdk(calls).languageModel(result.model.api.id)).toBeDefined()
+      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"])
     }),
   )

+ 10 - 5
packages/core/test/plugin/provider-venice.test.ts

@@ -41,7 +41,8 @@ describe("VenicePlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "venice-ai-sdk-provider",
         options: { name: "venice" },
@@ -58,7 +59,8 @@ describe("VenicePlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "venice-ai-sdk-provider",
         options: { name: "custom-venice", apiKey: "test" },
@@ -76,7 +78,8 @@ describe("VenicePlugin", () => {
       const similar = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "file:///tmp/venice-ai-sdk-provider.js",
         options: { name: "venice" },
@@ -84,7 +87,8 @@ describe("VenicePlugin", () => {
       const other = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
-          api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("model"),
+          package: "aisdk:test-provider",
         }),
         package: "@ai-sdk/openai-compatible",
         options: { name: "venice" },
@@ -103,7 +107,8 @@ describe("VenicePlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")),
-          api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "test-provider" },
+          modelID: ModelV2.ID.make("alias"),
+          package: "aisdk:test-provider",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},

+ 9 - 10
packages/core/test/plugin/provider-vercel.test.ts

@@ -25,12 +25,12 @@ describe("VercelPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
-          provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
-          provider.request.headers.Existing = "1"
+          provider.package = ProviderV2.aisdk("@ai-sdk/vercel")
+          provider.headers = { ...provider.headers, Existing: "1" }
         })
       })
       yield* addPlugin()
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).toEqual({
         Existing: "1",
         "http-referer": "https://opencode.ai/",
         "x-title": "opencode",
@@ -43,14 +43,12 @@ describe("VercelPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) =>
         catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
-          provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
+          provider.package = ProviderV2.aisdk("@ai-sdk/vercel")
         }),
       )
       yield* addPlugin()
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty(
-        "HTTP-Referer",
-      )
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty("X-Title")
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("HTTP-Referer")
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("X-Title")
     }),
   )
 
@@ -62,7 +60,8 @@ describe("VercelPlugin", () => {
       const event = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")),
-          api: { id: ModelV2.ID.make("v0-1.0-md"), type: "aisdk", package: "@ai-sdk/vercel" },
+          modelID: ModelV2.ID.make("v0-1.0-md"),
+          package: "aisdk:@ai-sdk/vercel",
         }),
         package: "@ai-sdk/vercel",
         options: { name: "custom-vercel" },
@@ -77,7 +76,7 @@ describe("VercelPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {}))
       yield* addPlugin()
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.headers).toBeUndefined()
     }),
   )
 })

+ 11 - 6
packages/core/test/plugin/provider-xai.test.ts

@@ -42,7 +42,8 @@ describe("XAIPlugin", () => {
       const ignored = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
-          api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
+          modelID: ModelV2.ID.make("grok-4"),
+          package: "aisdk:@ai-sdk/xai",
         }),
         package: "@ai-sdk/openai-compatible",
         options: {},
@@ -51,7 +52,8 @@ describe("XAIPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
-          api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
+          modelID: ModelV2.ID.make("grok-4"),
+          package: "aisdk:@ai-sdk/xai",
         }),
         package: "@ai-sdk/xai",
         options: {},
@@ -71,7 +73,8 @@ describe("XAIPlugin", () => {
       const result = yield* aisdk.runSDK({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")),
-          api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
+          modelID: ModelV2.ID.make("grok-4"),
+          package: "aisdk:@ai-sdk/xai",
         }),
         package: "@ai-sdk/xai",
         options: {},
@@ -81,7 +84,7 @@ describe("XAIPlugin", () => {
     }),
   )
 
-  it.effect("uses responses with the model api.id for xAI language models", () =>
+  it.effect("uses responses with the model modelID for xAI language models", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
@@ -91,7 +94,8 @@ describe("XAIPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")),
-          api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
+          modelID: ModelV2.ID.make("grok-4"),
+          package: "aisdk:@ai-sdk/xai",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},
@@ -112,7 +116,8 @@ describe("XAIPlugin", () => {
       const result = yield* aisdk.runLanguage({
         model: ModelV2.Info.make({
           ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")),
-          api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
+          modelID: ModelV2.ID.make("grok-4"),
+          package: "aisdk:@ai-sdk/xai",
         }),
         sdk: fakeSelectorSdk(calls),
         options: {},

+ 14 - 23
packages/core/test/plugin/provider-zenmux.test.ts

@@ -32,17 +32,14 @@ describe("ZenmuxPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://zenmux.ai/api/v1",
-          }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
         })
       })
       yield* addPlugin()
       const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux")))
-      expect(result.request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
-      expect(Object.keys(result.request.headers).sort()).toEqual(["HTTP-Referer", "X-Title"])
+      expect(result.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
+      expect(Object.keys(required(result.headers)).sort()).toEqual(["HTTP-Referer", "X-Title"])
     }),
   )
 
@@ -51,17 +48,14 @@ describe("ZenmuxPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://zenmux.ai/api/v1",
-          }
-          provider.request.headers.Existing = "value"
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
+          provider.headers = { ...provider.headers, Existing: "value" }
         })
       })
       yield* addPlugin()
 
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
@@ -74,17 +68,14 @@ describe("ZenmuxPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
-          provider.api = {
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://zenmux.ai/api/v1",
-          }
-          provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
+          provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
+          provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
         })
       })
       yield* addPlugin()
 
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({
         "HTTP-Referer": "https://example.com/",
         "X-Title": "custom-title",
       })
@@ -96,12 +87,12 @@ describe("ZenmuxPlugin", () => {
       const catalog = yield* Catalog.Service
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
-          provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
+          provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
         })
       })
       yield* addPlugin()
 
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).headers).toEqual({
         "HTTP-Referer": "https://example.com/",
         "X-Title": "custom-title",
       })

+ 5 - 11
packages/core/test/plugin/variant.test.ts

@@ -24,14 +24,11 @@ describe("VariantPlugin", () => {
       const service = yield* Catalog.Service
       yield* service.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
-          provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
+          provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
         })
         catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
-          model.api = {
-            id: ModelV2.ID.make("glm-5.2"),
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-          }
+          model.modelID = ModelV2.ID.make("glm-5.2")
+          model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
         })
       })
       yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
@@ -48,11 +45,8 @@ describe("VariantPlugin", () => {
       const service = yield* Catalog.Service
       yield* service.transform((catalog) => {
         catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
-          model.api = {
-            id: ModelV2.ID.make("glm-5.2"),
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-          }
+          model.modelID = ModelV2.ID.make("glm-5.2")
+          model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
           model.variants = [{ id: ModelV2.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }]
         })
       })

+ 2 - 3
packages/core/test/session-instructions.test.ts

@@ -2,8 +2,7 @@ import { describe, expect, test } from "bun:test"
 import fs from "fs/promises"
 import path from "path"
 import { DateTime, Effect, Layer } from "effect"
-import { Message, Model } from "@opencode-ai/llm"
-import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
+import { Message } from "@opencode-ai/llm"
 import { AgentV2 } from "@opencode-ai/core/agent"
 import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
@@ -300,7 +299,7 @@ describe("SessionInstructions", () => {
 
   test("toLLMMessages does not forward synthetic metadata to the provider", () => {
     const created = DateTime.makeUnsafe(0)
-    const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.route })
+    const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") })
     const synthetic = SessionMessage.Synthetic.make({
       id: SessionMessage.ID.make("msg_synthetic"),
       type: "synthetic",

+ 33 - 3
packages/core/test/session-runner-message.test.ts

@@ -1,6 +1,5 @@
 import { describe, expect, test } from "bun:test"
-import { Message, Model } from "@opencode-ai/llm"
-import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
+import { Message } from "@opencode-ai/llm"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { SessionMessage } from "@opencode-ai/core/session/message"
@@ -12,7 +11,7 @@ import { DateTime } from "effect"
 
 const created = DateTime.makeUnsafe(0)
 const id = (value: string) => SessionMessage.ID.make(`msg_${value}`)
-const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.route })
+const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") })
 
 describe("toLLMMessages", () => {
   test("omits empty assistant turns", () => {
@@ -609,4 +608,35 @@ Recent work
       },
     ])
   })
+
+  test("preserves provider metadata for a catalog alias with a different API model ID", () => {
+    const messages = toLLMMessages(
+      [
+        SessionMessage.Assistant.make({
+          id: id("assistant-alias"),
+          type: "assistant",
+          agent: "build",
+          model: { id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") },
+          content: [
+            SessionMessage.AssistantReasoning.make({
+              type: "reasoning",
+              id: "reasoning-alias",
+              text: "Visible thought",
+              providerMetadata: { openai: { reasoningEncryptedContent: "encrypted" } },
+            }),
+          ],
+          time: { created, completed: created },
+        }),
+      ],
+      ModelV2.Ref.make({ id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") }),
+    )
+
+    expect(messages[0]?.content).toEqual([
+      {
+        type: "reasoning",
+        text: "Visible thought",
+        providerMetadata: { openai: { reasoningEncryptedContent: "encrypted" } },
+      },
+    ])
+  })
 })

+ 246 - 90
packages/core/test/session-runner-model.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect } from "bun:test"
-import { LLM } from "@opencode-ai/llm"
+import { LLM, Model } from "@opencode-ai/llm"
 import { LLMClient } from "@opencode-ai/llm/route"
 import { DateTime, Effect } from "effect"
 import { Headers } from "effect/unstable/http"
@@ -13,28 +13,26 @@ import { SessionV2 } from "@opencode-ai/core/session"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { it } from "./lib/effect"
 
-type Api =
-  | {
-      readonly type: "aisdk"
-      readonly package: string
-      readonly url?: string
-      readonly settings?: Record<string, unknown>
-    }
-  | { readonly type: "native"; readonly url?: string; readonly settings: Record<string, unknown> }
+interface ModelOptions {
+  readonly modelID?: string
+  readonly settings?: ModelV2.Info["settings"]
+  readonly headers?: ModelV2.Info["headers"]
+  readonly body?: ModelV2.Info["body"]
+  readonly variants?: ModelV2.Info["variants"]
+}
 
-const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
+const model = (packageName: string | undefined, options: ModelOptions = {}) =>
   ModelV2.Info.make({
     id: ModelV2.ID.make("test-model"),
+    modelID: ModelV2.ID.make(options.modelID ?? "api-test-model"),
     providerID: ProviderV2.ID.make("test-provider"),
     name: "Test model",
-    api: { id: ModelV2.ID.make("api-test-model"), ...api },
+    package: packageName,
+    settings: options.settings ?? {},
+    headers: options.headers ?? { "x-test": "header" },
+    body: options.body ?? { custom_extension: { enabled: true } },
     capabilities: { tools: true, input: ["text"], output: ["text"] },
-    request: {
-      settings: {},
-      headers: { "x-test": "header" },
-      body: { apiKey: "secret", custom_extension: { enabled: true } },
-    },
-    variants,
+    variants: options.variants ?? [],
     time: { released: 0 },
     cost: [],
     status: "active",
@@ -43,12 +41,14 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
   })
 
 describe("SessionRunnerModel", () => {
-  it.effect("maps catalog OpenAI AI SDK models into native Responses routes", () =>
+  it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () =>
     Effect.gen(function* () {
-      const resolved = yield* SessionRunnerModel.fromCatalogModel(
-        model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-      )
+      const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
+        settings: { baseURL: "https://openai.example/v1" },
+      })
+      const resolved = yield* SessionRunnerModel.fromCatalogModel(catalog)
 
+      expect(catalog.id).toBe(ModelV2.ID.make("test-model"))
       expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
       expect(resolved.route).toMatchObject({
         id: "openai-responses",
@@ -65,7 +65,9 @@ describe("SessionRunnerModel", () => {
   it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
     Effect.gen(function* () {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
-        model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
+        model(ProviderV2.aisdk("@ai-sdk/openai"), {
+          settings: { apiKey: "secret", baseURL: "https://openai.example/v1" },
+        }),
       )
       const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
 
@@ -77,14 +79,14 @@ describe("SessionRunnerModel", () => {
   it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
     Effect.gen(function* () {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
-        ModelV2.Info.make({
-          ...model({
-            type: "aisdk",
-            package: "@ai-sdk/openai-compatible",
-            url: "https://compatible.example/v1",
-            settings: { apiKey: "settings-secret", compatibility: "strict" },
-          }),
-          request: { settings: {}, headers: {}, body: {} },
+        model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
+          settings: {
+            apiKey: "settings-secret",
+            baseURL: "https://compatible.example/v1",
+            compatibility: "strict",
+          },
+          headers: {},
+          body: {},
         }),
       )
       const request = LLM.request({ model: resolved, prompt: "Hello" })
@@ -97,24 +99,29 @@ describe("SessionRunnerModel", () => {
       })
 
       expect(headers.authorization).toBe("Bearer settings-secret")
+      expect(resolved.route.id).toBe("openai-compatible-chat")
+      expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1")
       expect(resolved.route.defaults.http?.body).toEqual({})
     }),
   )
 
   it.effect("overlays selected OpenAI Session variant settings and bodies", () =>
     Effect.gen(function* () {
-      const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [
-        {
-          id: ModelV2.VariantID.make("high"),
-          settings: { reasoningEffort: "high" },
-          headers: { "x-variant": "high" },
-          body: {
-            store: false,
-            service_tier: "priority",
-            temperature: 0.2,
+      const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
+        settings: { baseURL: "https://openai.example/v1" },
+        variants: [
+          {
+            id: ModelV2.VariantID.make("high"),
+            settings: { reasoningEffort: "high" },
+            headers: { "x-variant": "high" },
+            body: {
+              store: false,
+              service_tier: "priority",
+              temperature: 0.2,
+            },
           },
-        },
-      ])
+        ],
+      })
       const session = SessionV2.Info.make({
         id: SessionV2.ID.make("ses_model_variant"),
         projectID: ProjectV2.ID.global,
@@ -147,9 +154,9 @@ describe("SessionRunnerModel", () => {
 
   it.effect("overlays selected OpenAI-compatible Session variant bodies", () =>
     Effect.gen(function* () {
-      const catalog = model(
-        { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" },
-        [
+      const catalog = model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
+        settings: { baseURL: "https://compatible.example/v1" },
+        variants: [
           {
             id: ModelV2.VariantID.make("high"),
             settings: {},
@@ -157,7 +164,7 @@ describe("SessionRunnerModel", () => {
             body: { store: false, reasoning_effort: "high" },
           },
         ],
-      )
+      })
       const session = SessionV2.Info.make({
         id: SessionV2.ID.make("ses_compatible_variant"),
         projectID: ProjectV2.ID.global,
@@ -181,7 +188,9 @@ describe("SessionRunnerModel", () => {
 
   it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
     Effect.gen(function* () {
-      const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" })
+      const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
+        settings: { baseURL: "https://openai.example/v1" },
+      })
       const session = SessionV2.Info.make({
         id: SessionV2.ID.make("ses_model_variant_unavailable"),
         projectID: ProjectV2.ID.global,
@@ -211,14 +220,17 @@ describe("SessionRunnerModel", () => {
 
   it.effect("overlays selected Anthropic Session variant settings", () =>
     Effect.gen(function* () {
-      const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [
-        {
-          id: ModelV2.VariantID.make("high"),
-          settings: { thinking: { type: "enabled", budgetTokens: 12000 } },
-          headers: {},
-          body: {},
-        },
-      ])
+      const catalog = model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
+        settings: { baseURL: "https://anthropic.example/v1" },
+        variants: [
+          {
+            id: ModelV2.VariantID.make("high"),
+            settings: { thinking: { type: "enabled", budgetTokens: 12000 } },
+            headers: {},
+            body: {},
+          },
+        ],
+      })
       const session = SessionV2.Info.make({
         id: SessionV2.ID.make("ses_anthropic_variant"),
         projectID: ProjectV2.ID.global,
@@ -244,7 +256,9 @@ describe("SessionRunnerModel", () => {
   it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
     Effect.gen(function* () {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
-        model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }),
+        model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
+          settings: { baseURL: "https://anthropic.example/v1" },
+        }),
       )
 
       expect(resolved.route).toMatchObject({
@@ -257,9 +271,10 @@ describe("SessionRunnerModel", () => {
   it.effect("uses resolved credentials for bearer auth", () =>
     Effect.gen(function* () {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
-        ModelV2.Info.make({
-          ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-          request: { settings: {}, headers: {}, body: {} },
+        model(ProviderV2.aisdk("@ai-sdk/openai"), {
+          settings: { baseURL: "https://openai.example/v1" },
+          headers: {},
+          body: {},
         }),
         Credential.Key.make({ type: "key", key: "secret" }),
       )
@@ -280,9 +295,10 @@ describe("SessionRunnerModel", () => {
     Effect.gen(function* () {
       const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
-        ModelV2.Info.make({
-          ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-          request: { settings: {}, headers: {}, body: { apiKey: "configured-secret" } },
+        model(ProviderV2.aisdk("@ai-sdk/openai"), {
+          settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" },
+          headers: {},
+          body: {},
         }),
         credential,
       )
@@ -302,9 +318,10 @@ describe("SessionRunnerModel", () => {
   it.effect("does not project OAuth account metadata into the request body", () =>
     Effect.gen(function* () {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
-        ModelV2.Info.make({
-          ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-          request: { settings: {}, headers: {}, body: {} },
+        model(ProviderV2.aisdk("@ai-sdk/openai"), {
+          settings: { baseURL: "https://openai.example/v1" },
+          headers: {},
+          body: {},
         }),
         Credential.OAuth.make({
           type: "oauth",
@@ -323,9 +340,10 @@ describe("SessionRunnerModel", () => {
   it.effect("routes ChatGPT OAuth credentials to the codex backend", () =>
     Effect.gen(function* () {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
-        ModelV2.Info.make({
-          ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-          request: { headers: {}, body: {} },
+        model(ProviderV2.aisdk("@ai-sdk/openai"), {
+          settings: { baseURL: "https://openai.example/v1" },
+          headers: {},
+          body: {},
         }),
         Credential.OAuth.make({
           type: "oauth",
@@ -354,12 +372,78 @@ describe("SessionRunnerModel", () => {
     }),
   )
 
+  it.effect("routes native OpenAI provider packages with ChatGPT credentials to the codex backend", () =>
+    Effect.gen(function* () {
+      const resolved = yield* SessionRunnerModel.fromCatalogModel(
+        model("@opencode-ai/llm/providers/openai", {
+          settings: { baseURL: "https://openai.example/v1" },
+        }),
+        Credential.OAuth.make({
+          type: "oauth",
+          methodID: Integration.MethodID.make("chatgpt-browser"),
+          access: "chatgpt-token",
+          refresh: "refresh",
+          expires: Date.now() + 60_000,
+          metadata: { accountID: "acct_123" },
+        }),
+      )
+      const headers = yield* resolved.route.auth.apply({
+        request: LLM.request({ model: resolved, prompt: "Hello" }),
+        method: "POST",
+        url: "https://chatgpt.com/backend-api/codex/responses",
+        body: "{}",
+        headers: Headers.empty,
+      })
+
+      expect(resolved.route.endpoint.baseURL).toBe("https://chatgpt.com/backend-api/codex")
+      expect(headers.authorization).toBe("Bearer chatgpt-token")
+      expect(headers["chatgpt-account-id"]).toBe("acct_123")
+    }),
+  )
+
+  it.effect("does not route native OpenAI-compatible packages to the codex backend", () =>
+    Effect.gen(function* () {
+      const resolved = yield* SessionRunnerModel.fromCatalogModel(
+        model("@opencode-ai/llm/providers/openai-compatible", {
+          settings: { baseURL: "https://compatible.example/v1" },
+        }),
+        Credential.OAuth.make({
+          type: "oauth",
+          methodID: Integration.MethodID.make("chatgpt-browser"),
+          access: "chatgpt-token",
+          refresh: "refresh",
+          expires: Date.now() + 60_000,
+          metadata: { accountID: "acct_123" },
+        }),
+      )
+
+      expect(resolved.route.id).toBe("openai-compatible-chat")
+      expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1")
+    }),
+  )
+
+  it.effect("maps legacy OpenAI organization and project settings to headers", () =>
+    Effect.gen(function* () {
+      const resolved = yield* SessionRunnerModel.fromCatalogModel(
+        model(ProviderV2.aisdk("@ai-sdk/openai"), {
+          settings: { organization: "org_123", project: "proj_123" },
+        }),
+      )
+
+      expect(resolved.route.defaults.headers).toMatchObject({
+        "OpenAI-Organization": "org_123",
+        "OpenAI-Project": "proj_123",
+      })
+    }),
+  )
+
   it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () =>
     Effect.gen(function* () {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
-        ModelV2.Info.make({
-          ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-          request: { headers: {}, body: {} },
+        model(ProviderV2.aisdk("@ai-sdk/openai"), {
+          settings: { baseURL: "https://openai.example/v1" },
+          headers: {},
+          body: {},
         }),
         Credential.OAuth.make({
           type: "oauth",
@@ -387,9 +471,10 @@ describe("SessionRunnerModel", () => {
   it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () =>
     Effect.gen(function* () {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
-        ModelV2.Info.make({
-          ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-          request: { headers: {}, body: {} },
+        model(ProviderV2.aisdk("@ai-sdk/openai"), {
+          settings: { baseURL: "https://openai.example/v1" },
+          headers: {},
+          body: {},
         }),
         Credential.OAuth.make({
           type: "oauth",
@@ -415,35 +500,106 @@ describe("SessionRunnerModel", () => {
     }),
   )
 
-  it.effect("rejects catalog APIs without a native route", () =>
+  it.effect("loads dynamic native provider packages through the injected package loader", () =>
+    Effect.gen(function* () {
+      const native = yield* SessionRunnerModel.fromCatalogModel(
+        model(ProviderV2.aisdk("@ai-sdk/openai"), {
+          settings: { baseURL: "https://openai.example/v1" },
+        }),
+      )
+      const resolved = yield* SessionRunnerModel.fromCatalogModel(
+        model("@opencode-ai/llm/providers/custom", {
+          settings: { region: "test" },
+          headers: { "x-package": "header" },
+          body: { custom: true },
+        }),
+        undefined,
+        {
+          loadPackage: (specifier) => {
+            expect(specifier).toBe("@opencode-ai/llm/providers/custom")
+            return Effect.succeed({
+              model: (modelID, settings) => {
+                expect(modelID).toBe("api-test-model")
+                expect(settings).toEqual({
+                  region: "test",
+                  headers: { "x-package": "header" },
+                  body: { custom: true },
+                  limits: { context: 100, output: 20 },
+                })
+                return Model.make({ id: modelID, provider: "package-provider", route: native.route })
+              },
+            })
+          },
+        },
+      )
+
+      expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
+    }),
+  )
+
+  it.effect("loads arbitrary AISDK packages through the injected AISDK loader", () =>
+    Effect.gen(function* () {
+      const native = yield* SessionRunnerModel.fromCatalogModel(
+        model(ProviderV2.aisdk("@ai-sdk/openai"), {
+          settings: { baseURL: "https://openai.example/v1" },
+        }),
+      )
+      const resolved = yield* SessionRunnerModel.fromCatalogModel(
+        model(ProviderV2.aisdk("@ai-sdk/google"), {
+          modelID: "gemini-api-model",
+          settings: { project: "test" },
+          headers: { "x-aisdk": "header" },
+          body: { custom: true },
+        }),
+        Credential.Key.make({ type: "key", key: "fallback-secret" }),
+        {
+          loadAISDK: (runtime) =>
+            Effect.sync(() => {
+              expect(runtime).toMatchObject({
+                id: "test-model",
+                modelID: "gemini-api-model",
+                providerID: "test-provider",
+                package: ProviderV2.aisdk("@ai-sdk/google"),
+                settings: { project: "test", apiKey: "fallback-secret" },
+                headers: { "x-aisdk": "header" },
+                body: { custom: true },
+              })
+              return Model.make({
+                id: runtime.modelID ?? runtime.id,
+                provider: runtime.providerID,
+                route: native.route,
+              })
+            }),
+        },
+      )
+
+      expect(resolved).toMatchObject({ id: "gemini-api-model", provider: "test-provider" })
+    }),
+  )
+
+  it.effect("rejects AISDK packages without an available loader", () =>
     Effect.gen(function* () {
       const failure = yield* SessionRunnerModel.fromCatalogModel(
-        model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
+        model(ProviderV2.aisdk("@ai-sdk/google"), {
+          settings: { baseURL: "https://google.example/v1" },
+        }),
       ).pipe(Effect.flip)
 
       expect(failure).toMatchObject({
-        _tag: "SessionRunnerModel.UnsupportedApiError",
+        _tag: "SessionRunnerModel.UnsupportedPackageError",
         providerID: "test-provider",
         modelID: "test-model",
-        api: "aisdk:@ai-sdk/google",
+        package: "aisdk:@ai-sdk/google",
       })
-      expect(failure.message).toBe("Unsupported API for test-provider/test-model: aisdk:@ai-sdk/google")
+      expect(failure.message).toBe("Unsupported package for test-provider/test-model: aisdk:@ai-sdk/google")
     }),
   )
 
-  it.effect("reports whether a catalog model has a supported native route", () =>
+  it.effect("reports whether a catalog model declares a provider package", () =>
     Effect.sync(() => {
-      expect(
-        SessionRunnerModel.supported(
-          model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-        ),
-      ).toBe(true)
-      expect(
-        SessionRunnerModel.supported(
-          model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
-        ),
-      ).toBe(false)
-      expect(SessionRunnerModel.supported(model({ type: "native", settings: {} }))).toBe(false)
+      expect(SessionRunnerModel.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true)
+      expect(SessionRunnerModel.supported(model("@opencode-ai/llm/providers/custom"))).toBe(true)
+      expect(SessionRunnerModel.supported(model(undefined))).toBe(false)
     }),
   )
 })

+ 0 - 4
packages/core/test/shared-schema.test.ts

@@ -112,12 +112,8 @@ test("Core reuses the canonical shared schemas", async () => {
     [ModelV2.Family, Model.Family],
     [ModelV2.Capabilities, Model.Capabilities],
     [ModelV2.Cost, Model.Cost],
-    [ModelV2.Api, Model.Api],
     [ModelV2.Info, Model.Info],
     [ProviderV2.ID, Provider.ID],
-    [ProviderV2.AISDK, Provider.AISDK],
-    [ProviderV2.Native, Provider.Native],
-    [ProviderV2.Api, Provider.Api],
     [ProviderV2.Request, Provider.Request],
     [ProviderV2.Info, Provider.Info],
     [corePermission.Effect, Permission.Effect],

+ 3 - 0
packages/llm/package.json

@@ -15,6 +15,7 @@
     "./route": "./src/route/index.ts",
     "./provider": "./src/provider.ts",
     "./providers": "./src/providers/index.ts",
+    "./provider-package": "./src/provider-package.ts",
     "./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
     "./providers/anthropic": "./src/providers/anthropic.ts",
     "./providers/azure": "./src/providers/azure.ts",
@@ -22,6 +23,8 @@
     "./providers/github-copilot": "./src/providers/github-copilot.ts",
     "./providers/google": "./src/providers/google.ts",
     "./providers/openai": "./src/providers/openai.ts",
+    "./providers/openai/responses": "./src/providers/openai/responses.ts",
+    "./providers/openai/chat": "./src/providers/openai/chat.ts",
     "./providers/openai-compatible": "./src/providers/openai-compatible.ts",
     "./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts",
     "./providers/openrouter": "./src/providers/openrouter.ts",

+ 2 - 0
packages/llm/src/index.ts

@@ -1,6 +1,7 @@
 export { LLMClient } from "./route/client"
 export { Auth } from "./route/auth"
 export { Provider } from "./provider"
+export { ProviderPackage } from "./provider-package"
 export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
 export type {
   RouteModelInput,
@@ -31,3 +32,4 @@ export type {
   ModelFactory as ProviderModelFactory,
   ModelOptions as ProviderModelOptions,
 } from "./provider"
+export type { Definition as ProviderPackageDefinition, Settings as ProviderPackageSettings } from "./provider-package"

+ 16 - 0
packages/llm/src/provider-package.ts

@@ -0,0 +1,16 @@
+import type { Model } from "./schema"
+
+export interface Settings extends Readonly<Record<string, unknown>> {
+  readonly headers?: Readonly<Record<string, string>>
+  readonly body?: Readonly<Record<string, unknown>>
+  readonly limits?: {
+    readonly context: number
+    readonly output: number
+  }
+}
+
+export interface Definition<ProviderSettings extends Settings = Settings> {
+  readonly model: (modelID: string, settings: ProviderSettings) => Model
+}
+
+export * as ProviderPackage from "./provider-package"

+ 26 - 1
packages/llm/src/providers/amazon-bedrock.ts

@@ -1,5 +1,6 @@
 import type { RouteDefaultsInput } from "../route/client"
 import { Auth } from "../route/auth"
+import type { ProviderPackage } from "../provider-package"
 import { ProviderID, type ModelID } from "../schema"
 import * as BedrockConverse from "../protocols/bedrock-converse"
 import type { BedrockCredentials } from "../protocols/bedrock-converse"
@@ -15,6 +16,15 @@ export type Config = RouteDefaultsInput & {
   /** Override the computed `https://bedrock-runtime.<region>.amazonaws.com` URL. */
   readonly baseURL?: string
 }
+
+export interface Settings extends ProviderPackage.Settings {
+  readonly apiKey?: string
+  readonly auth?: "bearer" | "sigv4"
+  readonly baseURL?: string
+  readonly credentials?: BedrockCredentials
+  readonly region?: string
+  readonly topP?: number
+}
 export const routes = [BedrockConverse.route]
 
 const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com`
@@ -40,4 +50,19 @@ export const configure = (input: Config = {}) => {
 }
 
 export const provider = configure()
-export const model = provider.model
+export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
+  if (settings.auth === "bearer" && settings.apiKey === undefined)
+    throw new Error("Amazon Bedrock bearer auth requires apiKey")
+  if (settings.auth === "sigv4" && settings.apiKey !== undefined)
+    throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey")
+  return configure({
+    apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey,
+    baseURL: settings.baseURL,
+    credentials: settings.credentials,
+    generation: settings.topP === undefined ? undefined : { topP: settings.topP },
+    headers: settings.headers === undefined ? undefined : { ...settings.headers },
+    http: settings.body === undefined ? undefined : { body: { ...settings.body } },
+    limits: settings.limits,
+    region: settings.region,
+  }).model(modelID)
+}

+ 15 - 1
packages/llm/src/providers/anthropic.ts

@@ -1,6 +1,7 @@
 import type { RouteDefaultsInput } from "../route/client"
 import { Auth } from "../route/auth"
 import type { ProviderAuthOption } from "../route/auth-options"
+import type { ProviderPackage } from "../provider-package"
 import { ProviderID, type ModelID } from "../schema"
 import * as AnthropicMessages from "../protocols/anthropic-messages"
 
@@ -10,6 +11,12 @@ export const routes = [AnthropicMessages.route]
 
 export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
 
+export interface Settings extends ProviderPackage.Settings {
+  readonly apiKey?: string
+  readonly authToken?: string
+  readonly baseURL?: string
+}
+
 const auth = (options: ProviderAuthOption<"optional">) => {
   if ("auth" in options && options.auth) return options.auth
   return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey")
@@ -32,4 +39,11 @@ export const configure = (input: Config = {}) => {
 }
 
 export const provider = configure()
-export const model = provider.model
+export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
+  configure({
+    ...(settings.authToken === undefined ? { apiKey: settings.apiKey } : { auth: Auth.bearer(settings.authToken) }),
+    baseURL: settings.baseURL,
+    headers: settings.headers === undefined ? undefined : { ...settings.headers },
+    http: settings.body === undefined ? undefined : { body: { ...settings.body } },
+    limits: settings.limits,
+  }).model(modelID)

+ 17 - 0
packages/llm/src/providers/openai-compatible.ts

@@ -2,6 +2,7 @@ import { ProviderID, type ModelID } from "../schema"
 import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
 import type { RouteDefaultsInput } from "../route/client"
 import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
+import type { ProviderPackage } from "../provider-package"
 import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
 
 export const id = ProviderID.make("openai-compatible")
@@ -12,6 +13,12 @@ type GenericModelOptions = RouteDefaultsInput &
     readonly baseURL: string
   }
 
+export interface Settings extends ProviderPackage.Settings {
+  readonly apiKey?: string
+  readonly baseURL: string
+  readonly provider?: string
+}
+
 export type FamilyModelOptions = RouteDefaultsInput &
   ProviderAuthOption<"optional"> & {
     readonly baseURL?: string
@@ -56,6 +63,16 @@ export const provider = {
   configure,
 }
 
+export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
+  configure({
+    apiKey: settings.apiKey,
+    baseURL: settings.baseURL,
+    headers: settings.headers === undefined ? undefined : { ...settings.headers },
+    http: settings.body === undefined ? undefined : { body: { ...settings.body } },
+    limits: settings.limits,
+    provider: settings.provider,
+  }).model(modelID)
+
 export const baseten = define(profiles.baseten)
 export const cerebras = define(profiles.cerebras)
 export const deepinfra = define(profiles.deepinfra)

+ 37 - 1
packages/llm/src/providers/openai.ts

@@ -1,5 +1,6 @@
 import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
 import type { Route, RouteDefaultsInput } from "../route/client"
+import type { ProviderPackage } from "../provider-package"
 import { ProviderID, type ModelID } from "../schema"
 import * as OpenAIChat from "../protocols/openai-chat"
 import * as OpenAIResponses from "../protocols/openai-responses"
@@ -21,6 +22,16 @@ export type Config = RouteDefaultsInput &
     readonly providerOptions?: OpenAIProviderOptionsInput
   }
 
+export interface Settings extends ProviderPackage.Settings {
+  readonly apiKey?: string
+  readonly baseURL?: string
+  readonly organization?: string
+  readonly project?: string
+  readonly queryParams?: Readonly<Record<string, string>>
+  readonly transport?: "http" | "websocket"
+  readonly providerOptions?: OpenAIProviderOptionsInput
+}
+
 const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
 
 const defaults = (input: Config) => {
@@ -57,7 +68,32 @@ export const configure = (input: Config = {}) => {
 
 export const provider = configure()
 
-export const model = provider.model
+const config = (settings: Settings): Config => {
+  const headers = {
+    ...(settings.organization === undefined ? {} : { "OpenAI-Organization": settings.organization }),
+    ...(settings.project === undefined ? {} : { "OpenAI-Project": settings.project }),
+    ...settings.headers,
+  }
+  return {
+    apiKey: settings.apiKey,
+    baseURL: settings.baseURL,
+    headers: Object.keys(headers).length === 0 ? undefined : headers,
+    http: settings.body === undefined ? undefined : { body: { ...settings.body } },
+    limits: settings.limits,
+    providerOptions: settings.providerOptions,
+    queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
+  }
+}
+
+export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
+  const configured = configure(config(settings))
+  if (settings.transport === undefined || settings.transport === "http") return configured.responses(modelID)
+  if (settings.transport === "websocket") return configured.responsesWebSocket(modelID)
+  throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`)
+}
+
+export const chatModel: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
+  configure(config(settings)).chat(modelID)
 export const responses = provider.responses
 export const responsesWebSocket = provider.responsesWebSocket
 export const chat = provider.chat

+ 2 - 0
packages/llm/src/providers/openai/chat.ts

@@ -0,0 +1,2 @@
+export { chatModel as model } from "../openai"
+export type { Settings } from "../openai"

+ 2 - 0
packages/llm/src/providers/openai/responses.ts

@@ -0,0 +1,2 @@
+export { model } from "../openai"
+export type { Settings } from "../openai"

+ 0 - 1
packages/llm/test/exports.test.ts

@@ -30,7 +30,6 @@ describe("public exports", () => {
 
   test("provider barrels expose user-facing facades", () => {
     expect(OpenAI.model).toBeFunction()
-    expect(OpenAI.provider.model).toBe(OpenAI.model)
     expect(OpenAI.provider.responses).toBe(OpenAI.responses)
     expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
     expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff