Ver código fonte

core: refactor provider and model system (#5033)

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: thdxr <[email protected]>
Dax 2 meses atrás
pai
commit
6d3fc63658

+ 2 - 2
packages/desktop/src/components/prompt-input.tsx

@@ -456,9 +456,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                     <img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0" />
                     <img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0" />
                     <div class="flex gap-x-3 items-baseline flex-[1_0_0]">
                     <div class="flex gap-x-3 items-baseline flex-[1_0_0]">
                       <span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
                       <span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
-                      <Show when={i.release_date}>
+                      <Show when={false}>
                         <span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
                         <span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
-                          {DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
+                          {DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")}
                         </span>
                         </span>
                       </Show>
                       </Show>
                     </div>
                     </div>

+ 2 - 1
packages/opencode/src/agent/agent.ts

@@ -224,6 +224,7 @@ export namespace Agent {
   export async function generate(input: { description: string }) {
   export async function generate(input: { description: string }) {
     const defaultModel = await Provider.defaultModel()
     const defaultModel = await Provider.defaultModel()
     const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
     const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
+    const language = await Provider.getLanguage(model)
     const system = SystemPrompt.header(defaultModel.providerID)
     const system = SystemPrompt.header(defaultModel.providerID)
     system.push(PROMPT_GENERATE)
     system.push(PROMPT_GENERATE)
     const existing = await list()
     const existing = await list()
@@ -241,7 +242,7 @@ export namespace Agent {
           content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n  Return ONLY the JSON object, no other text, do not wrap in backticks`,
           content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n  Return ONLY the JSON object, no other text, do not wrap in backticks`,
         },
         },
       ],
       ],
-      model: model.language,
+      model: language,
       schema: z.object({
       schema: z.object({
         identifier: z.string(),
         identifier: z.string(),
         whenToUse: z.string(),
         whenToUse: z.string(),

+ 1 - 1
packages/opencode/src/cli/cmd/models.ts

@@ -38,7 +38,7 @@ export const ModelsCommand = cmd({
 
 
         function printModels(providerID: string, verbose?: boolean) {
         function printModels(providerID: string, verbose?: boolean) {
           const provider = providers[providerID]
           const provider = providers[providerID]
-          const sortedModels = Object.entries(provider.info.models).sort(([a], [b]) => a.localeCompare(b))
+          const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
           for (const [modelID, model] of sortedModels) {
           for (const [modelID, model] of sortedModels) {
             process.stdout.write(`${providerID}/${modelID}`)
             process.stdout.write(`${providerID}/${modelID}`)
             process.stdout.write(EOL)
             process.stdout.write(EOL)

+ 37 - 37
packages/opencode/src/config/config.ts

@@ -470,6 +470,42 @@ export namespace Config {
   })
   })
   export type Layout = z.infer<typeof Layout>
   export type Layout = z.infer<typeof Layout>
 
 
+  export const Provider = ModelsDev.Provider.partial()
+    .extend({
+      whitelist: z.array(z.string()).optional(),
+      blacklist: z.array(z.string()).optional(),
+      models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
+      options: z
+        .object({
+          apiKey: z.string().optional(),
+          baseURL: z.string().optional(),
+          enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
+          setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"),
+          timeout: z
+            .union([
+              z
+                .number()
+                .int()
+                .positive()
+                .describe(
+                  "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+                ),
+              z.literal(false).describe("Disable timeout for this provider entirely."),
+            ])
+            .optional()
+            .describe(
+              "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+            ),
+        })
+        .catchall(z.any())
+        .optional(),
+    })
+    .strict()
+    .meta({
+      ref: "ProviderConfig",
+    })
+  export type Provider = z.infer<typeof Provider>
+
   export const Info = z
   export const Info = z
     .object({
     .object({
       $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
       $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
@@ -536,43 +572,7 @@ export namespace Config {
         .optional()
         .optional()
         .describe("Agent configuration, see https://opencode.ai/docs/agent"),
         .describe("Agent configuration, see https://opencode.ai/docs/agent"),
       provider: z
       provider: z
-        .record(
-          z.string(),
-          ModelsDev.Provider.partial()
-            .extend({
-              whitelist: z.array(z.string()).optional(),
-              blacklist: z.array(z.string()).optional(),
-              models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
-              options: z
-                .object({
-                  apiKey: z.string().optional(),
-                  baseURL: z.string().optional(),
-                  enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
-                  setCacheKey: z
-                    .boolean()
-                    .optional()
-                    .describe("Enable promptCacheKey for this provider (default false)"),
-                  timeout: z
-                    .union([
-                      z
-                        .number()
-                        .int()
-                        .positive()
-                        .describe(
-                          "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
-                        ),
-                      z.literal(false).describe("Disable timeout for this provider entirely."),
-                    ])
-                    .optional()
-                    .describe(
-                      "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
-                    ),
-                })
-                .catchall(z.any())
-                .optional(),
-            })
-            .strict(),
-        )
+        .record(z.string(), Provider)
         .optional()
         .optional()
         .describe("Custom provider configurations and model overrides"),
         .describe("Custom provider configurations and model overrides"),
       mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
       mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),

+ 36 - 42
packages/opencode/src/provider/models.ts

@@ -9,16 +9,16 @@ export namespace ModelsDev {
   const log = Log.create({ service: "models.dev" })
   const log = Log.create({ service: "models.dev" })
   const filepath = path.join(Global.Path.cache, "models.json")
   const filepath = path.join(Global.Path.cache, "models.json")
 
 
-  export const Model = z
-    .object({
-      id: z.string(),
-      name: z.string(),
-      release_date: z.string(),
-      attachment: z.boolean(),
-      reasoning: z.boolean(),
-      temperature: z.boolean(),
-      tool_call: z.boolean(),
-      cost: z.object({
+  export const Model = z.object({
+    id: z.string(),
+    name: z.string(),
+    release_date: z.string(),
+    attachment: z.boolean(),
+    reasoning: z.boolean(),
+    temperature: z.boolean(),
+    tool_call: z.boolean(),
+    cost: z
+      .object({
         input: z.number(),
         input: z.number(),
         output: z.number(),
         output: z.number(),
         cache_read: z.number().optional(),
         cache_read: z.number().optional(),
@@ -31,40 +31,34 @@ export namespace ModelsDev {
             cache_write: z.number().optional(),
             cache_write: z.number().optional(),
           })
           })
           .optional(),
           .optional(),
-      }),
-      limit: z.object({
-        context: z.number(),
-        output: z.number(),
-      }),
-      modalities: z
-        .object({
-          input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
-          output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
-        })
-        .optional(),
-      experimental: z.boolean().optional(),
-      status: z.enum(["alpha", "beta", "deprecated"]).optional(),
-      options: z.record(z.string(), z.any()),
-      headers: z.record(z.string(), z.string()).optional(),
-      provider: z.object({ npm: z.string() }).optional(),
-    })
-    .meta({
-      ref: "Model",
-    })
+      })
+      .optional(),
+    limit: z.object({
+      context: z.number(),
+      output: z.number(),
+    }),
+    modalities: z
+      .object({
+        input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+        output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+      })
+      .optional(),
+    experimental: z.boolean().optional(),
+    status: z.enum(["alpha", "beta", "deprecated"]).optional(),
+    options: z.record(z.string(), z.any()),
+    headers: z.record(z.string(), z.string()).optional(),
+    provider: z.object({ npm: z.string() }).optional(),
+  })
   export type Model = z.infer<typeof Model>
   export type Model = z.infer<typeof Model>
 
 
-  export const Provider = z
-    .object({
-      api: z.string().optional(),
-      name: z.string(),
-      env: z.array(z.string()),
-      id: z.string(),
-      npm: z.string().optional(),
-      models: z.record(z.string(), Model),
-    })
-    .meta({
-      ref: "Provider",
-    })
+  export const Provider = z.object({
+    api: z.string().optional(),
+    name: z.string(),
+    env: z.array(z.string()),
+    id: z.string(),
+    npm: z.string().optional(),
+    models: z.record(z.string(), Model),
+  })
 
 
   export type Provider = z.infer<typeof Provider>
   export type Provider = z.infer<typeof Provider>
 
 

+ 316 - 193
packages/opencode/src/provider/provider.ts

@@ -1,8 +1,8 @@
 import z from "zod"
 import z from "zod"
 import fuzzysort from "fuzzysort"
 import fuzzysort from "fuzzysort"
 import { Config } from "../config/config"
 import { Config } from "../config/config"
-import { mergeDeep, sortBy } from "remeda"
-import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
+import { mapValues, mergeDeep, sortBy } from "remeda"
+import { NoSuchModelError, type Provider as SDK } from "ai"
 import { Log } from "../util/log"
 import { Log } from "../util/log"
 import { BunProc } from "../bun"
 import { BunProc } from "../bun"
 import { Plugin } from "../plugin"
 import { Plugin } from "../plugin"
@@ -23,7 +23,7 @@ import { createVertex } from "@ai-sdk/google-vertex"
 import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
 import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
 import { createOpenAI } from "@ai-sdk/openai"
 import { createOpenAI } from "@ai-sdk/openai"
 import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
 import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
-import { createOpenRouter } from "@openrouter/ai-sdk-provider"
+import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider"
 import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
 import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
 
 
 export namespace Provider {
 export namespace Provider {
@@ -43,14 +43,13 @@ export namespace Provider {
     "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
     "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
   }
   }
 
 
-  type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
+  type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
+  type CustomLoader = (provider: Info) => Promise<{
     autoload: boolean
     autoload: boolean
-    getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
+    getModel?: CustomModelLoader
     options?: Record<string, any>
     options?: Record<string, any>
   }>
   }>
 
 
-  type Source = "env" | "config" | "custom" | "api"
-
   const CUSTOM_LOADERS: Record<string, CustomLoader> = {
   const CUSTOM_LOADERS: Record<string, CustomLoader> = {
     async anthropic() {
     async anthropic() {
       return {
       return {
@@ -280,7 +279,7 @@ export namespace Provider {
           project,
           project,
           location,
           location,
         },
         },
-        async getModel(sdk: any, modelID: string) {
+        async getModel(sdk, modelID) {
           const id = String(modelID).trim()
           const id = String(modelID).trim()
           return sdk.languageModel(id)
           return sdk.languageModel(id)
         },
         },
@@ -299,10 +298,155 @@ export namespace Provider {
     },
     },
   }
   }
 
 
+  export const Model = z
+    .object({
+      id: z.string(),
+      providerID: z.string(),
+      api: z.object({
+        id: z.string(),
+        url: z.string(),
+        npm: z.string(),
+      }),
+      name: z.string(),
+      capabilities: z.object({
+        temperature: z.boolean(),
+        reasoning: z.boolean(),
+        attachment: z.boolean(),
+        toolcall: z.boolean(),
+        input: z.object({
+          text: z.boolean(),
+          audio: z.boolean(),
+          image: z.boolean(),
+          video: z.boolean(),
+          pdf: z.boolean(),
+        }),
+        output: z.object({
+          text: z.boolean(),
+          audio: z.boolean(),
+          image: z.boolean(),
+          video: z.boolean(),
+          pdf: z.boolean(),
+        }),
+      }),
+      cost: z.object({
+        input: z.number(),
+        output: z.number(),
+        cache: z.object({
+          read: z.number(),
+          write: z.number(),
+        }),
+        experimentalOver200K: z
+          .object({
+            input: z.number(),
+            output: z.number(),
+            cache: z.object({
+              read: z.number(),
+              write: z.number(),
+            }),
+          })
+          .optional(),
+      }),
+      limit: z.object({
+        context: z.number(),
+        output: z.number(),
+      }),
+      status: z.enum(["alpha", "beta", "deprecated", "active"]),
+      options: z.record(z.string(), z.any()),
+      headers: z.record(z.string(), z.string()),
+    })
+    .meta({
+      ref: "Model",
+    })
+  export type Model = z.infer<typeof Model>
+
+  export const Info = z
+    .object({
+      id: z.string(),
+      name: z.string(),
+      source: z.enum(["env", "config", "custom", "api"]),
+      env: z.string().array(),
+      key: z.string().optional(),
+      options: z.record(z.string(), z.any()),
+      models: z.record(z.string(), Model),
+    })
+    .meta({
+      ref: "Provider",
+    })
+  export type Info = z.infer<typeof Info>
+
+  function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
+    return {
+      id: model.id,
+      providerID: provider.id,
+      name: model.name,
+      api: {
+        id: model.id,
+        url: provider.api!,
+        npm: model.provider?.npm ?? provider.npm ?? provider.id,
+      },
+      status: model.status ?? "active",
+      headers: model.headers ?? {},
+      options: model.options ?? {},
+      cost: {
+        input: model.cost?.input ?? 0,
+        output: model.cost?.output ?? 0,
+        cache: {
+          read: model.cost?.cache_read ?? 0,
+          write: model.cost?.cache_write ?? 0,
+        },
+        experimentalOver200K: model.cost?.context_over_200k
+          ? {
+              cache: {
+                read: model.cost.context_over_200k.cache_read ?? 0,
+                write: model.cost.context_over_200k.cache_write ?? 0,
+              },
+              input: model.cost.context_over_200k.input,
+              output: model.cost.context_over_200k.output,
+            }
+          : undefined,
+      },
+      limit: {
+        context: model.limit.context,
+        output: model.limit.output,
+      },
+      capabilities: {
+        temperature: model.temperature,
+        reasoning: model.reasoning,
+        attachment: model.attachment,
+        toolcall: model.tool_call,
+        input: {
+          text: model.modalities?.input?.includes("text") ?? false,
+          audio: model.modalities?.input?.includes("audio") ?? false,
+          image: model.modalities?.input?.includes("image") ?? false,
+          video: model.modalities?.input?.includes("video") ?? false,
+          pdf: model.modalities?.input?.includes("pdf") ?? false,
+        },
+        output: {
+          text: model.modalities?.output?.includes("text") ?? false,
+          audio: model.modalities?.output?.includes("audio") ?? false,
+          image: model.modalities?.output?.includes("image") ?? false,
+          video: model.modalities?.output?.includes("video") ?? false,
+          pdf: model.modalities?.output?.includes("pdf") ?? false,
+        },
+      },
+    }
+  }
+
+  export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
+    return {
+      id: provider.id,
+      source: "custom",
+      name: provider.name,
+      env: provider.env ?? [],
+      options: {},
+      models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)),
+    }
+  }
+
   const state = Instance.state(async () => {
   const state = Instance.state(async () => {
     using _ = log.time("state")
     using _ = log.time("state")
     const config = await Config.get()
     const config = await Config.get()
-    const database = await ModelsDev.get()
+    const database = mapValues(await ModelsDev.get(), fromModelsDevProvider)
 
 
     const disabled = new Set(config.disabled_providers ?? [])
     const disabled = new Set(config.disabled_providers ?? [])
     const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
     const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
@@ -313,54 +457,15 @@ export namespace Provider {
       return true
       return true
     }
     }
 
 
-    const providers: {
-      [providerID: string]: {
-        source: Source
-        info: ModelsDev.Provider
-        getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
-        options: Record<string, any>
-      }
+    const providers: { [providerID: string]: Info } = {}
+    const languages = new Map<string, LanguageModelV2>()
+    const modelLoaders: {
+      [providerID: string]: CustomModelLoader
     } = {}
     } = {}
-    const models = new Map<
-      string,
-      {
-        providerID: string
-        modelID: string
-        info: ModelsDev.Model
-        language: LanguageModel
-        npm?: string
-      }
-    >()
     const sdk = new Map<number, SDK>()
     const sdk = new Map<number, SDK>()
-    // Maps `${provider}/${key}` to the provider’s actual model ID for custom aliases.
-    const realIdByKey = new Map<string, string>()
 
 
     log.info("init")
     log.info("init")
 
 
-    function mergeProvider(
-      id: string,
-      options: Record<string, any>,
-      source: Source,
-      getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>,
-    ) {
-      const provider = providers[id]
-      if (!provider) {
-        const info = database[id]
-        if (!info) return
-        if (info.api && !options["baseURL"]) options["baseURL"] = info.api
-        providers[id] = {
-          source,
-          info,
-          options,
-          getModel,
-        }
-        return
-      }
-      provider.options = mergeDeep(provider.options, options)
-      provider.source = source
-      provider.getModel = getModel ?? provider.getModel
-    }
-
     const configProviders = Object.entries(config.provider ?? {})
     const configProviders = Object.entries(config.provider ?? {})
 
 
     // Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot
     // Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot
@@ -370,19 +475,31 @@ export namespace Provider {
         ...githubCopilot,
         ...githubCopilot,
         id: "github-copilot-enterprise",
         id: "github-copilot-enterprise",
         name: "GitHub Copilot Enterprise",
         name: "GitHub Copilot Enterprise",
-        // Enterprise uses a different API endpoint - will be set dynamically based on auth
-        api: undefined,
       }
       }
     }
     }
 
 
+    function mergeProvider(providerID: string, provider: Partial<Info>) {
+      const existing = providers[providerID]
+      if (existing) {
+        // @ts-expect-error
+        providers[providerID] = mergeDeep(existing, provider)
+        return
+      }
+      const match = database[providerID]
+      if (!match) return
+      // @ts-expect-error
+      providers[providerID] = mergeDeep(match, provider)
+    }
+
+    // extend database from config
     for (const [providerID, provider] of configProviders) {
     for (const [providerID, provider] of configProviders) {
       const existing = database[providerID]
       const existing = database[providerID]
-      const parsed: ModelsDev.Provider = {
+      const parsed: Info = {
         id: providerID,
         id: providerID,
-        npm: provider.npm ?? existing?.npm,
         name: provider.name ?? existing?.name ?? providerID,
         name: provider.name ?? existing?.name ?? providerID,
         env: provider.env ?? existing?.env ?? [],
         env: provider.env ?? existing?.env ?? [],
-        api: provider.api ?? existing?.api,
+        options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
+        source: "config",
         models: existing?.models ?? {},
         models: existing?.models ?? {},
       }
       }
 
 
@@ -393,51 +510,53 @@ export namespace Provider {
           if (model.id && model.id !== modelID) return modelID
           if (model.id && model.id !== modelID) return modelID
           return existing?.name ?? modelID
           return existing?.name ?? modelID
         })
         })
-        const parsedModel: ModelsDev.Model = {
+        const parsedModel: Model = {
           id: modelID,
           id: modelID,
-          name,
-          release_date: model.release_date ?? existing?.release_date,
-          attachment: model.attachment ?? existing?.attachment ?? false,
-          reasoning: model.reasoning ?? existing?.reasoning ?? false,
-          temperature: model.temperature ?? existing?.temperature ?? false,
-          tool_call: model.tool_call ?? existing?.tool_call ?? true,
-          cost:
-            !model.cost && !existing?.cost
-              ? {
-                  input: 0,
-                  output: 0,
-                  cache_read: 0,
-                  cache_write: 0,
-                }
-              : {
-                  cache_read: 0,
-                  cache_write: 0,
-                  ...existing?.cost,
-                  ...model.cost,
-                },
-          options: {
-            ...existing?.options,
-            ...model.options,
+          api: {
+            id: model.id ?? existing?.api.id ?? modelID,
+            npm: model.provider?.npm ?? provider.npm ?? existing?.api.npm ?? providerID,
+            url: provider?.api ?? existing?.api.url,
           },
           },
-          limit: model.limit ??
-            existing?.limit ?? {
-              context: 0,
-              output: 0,
+          status: model.status ?? existing?.status ?? "active",
+          name,
+          providerID,
+          capabilities: {
+            temperature: model.temperature ?? existing?.capabilities.temperature ?? false,
+            reasoning: model.reasoning ?? existing?.capabilities.reasoning ?? false,
+            attachment: model.attachment ?? existing?.capabilities.attachment ?? false,
+            toolcall: model.tool_call ?? existing?.capabilities.toolcall ?? true,
+            input: {
+              text: model.modalities?.input?.includes("text") ?? existing?.capabilities.input.text ?? true,
+              audio: model.modalities?.input?.includes("audio") ?? existing?.capabilities.input.audio ?? false,
+              image: model.modalities?.input?.includes("image") ?? existing?.capabilities.input.image ?? false,
+              video: model.modalities?.input?.includes("video") ?? existing?.capabilities.input.video ?? false,
+              pdf: model.modalities?.input?.includes("pdf") ?? existing?.capabilities.input.pdf ?? false,
             },
             },
-          modalities: model.modalities ??
-            existing?.modalities ?? {
-              input: ["text"],
-              output: ["text"],
+            output: {
+              text: model.modalities?.output?.includes("text") ?? existing?.capabilities.output.text ?? true,
+              audio: model.modalities?.output?.includes("audio") ?? existing?.capabilities.output.audio ?? false,
+              image: model.modalities?.output?.includes("image") ?? existing?.capabilities.output.image ?? false,
+              video: model.modalities?.output?.includes("video") ?? existing?.capabilities.output.video ?? false,
+              pdf: model.modalities?.output?.includes("pdf") ?? existing?.capabilities.output.pdf ?? false,
             },
             },
-          headers: model.headers,
-          provider: model.provider ?? existing?.provider,
-        }
-        if (model.id && model.id !== modelID) {
-          realIdByKey.set(`${providerID}/${modelID}`, model.id)
+          },
+          cost: {
+            input: model?.cost?.input ?? existing?.cost?.input ?? 0,
+            output: model?.cost?.output ?? existing?.cost?.output ?? 0,
+            cache: {
+              read: model?.cost?.cache_read ?? existing?.cost?.cache.read ?? 0,
+              write: model?.cost?.cache_write ?? existing?.cost?.cache.write ?? 0,
+            },
+          },
+          options: mergeDeep(existing?.options ?? {}, model.options ?? {}),
+          limit: {
+            context: model.limit?.context ?? existing?.limit?.context ?? 0,
+            output: model.limit?.output ?? existing?.limit?.output ?? 0,
+          },
+          headers: mergeDeep(existing?.headers ?? {}, model.headers ?? {}),
         }
         }
         parsed.models[modelID] = parsedModel
         parsed.models[modelID] = parsedModel
       }
       }
-
       database[providerID] = parsed
       database[providerID] = parsed
     }
     }
 
 
@@ -447,19 +566,20 @@ export namespace Provider {
       if (disabled.has(providerID)) continue
       if (disabled.has(providerID)) continue
       const apiKey = provider.env.map((item) => env[item]).find(Boolean)
       const apiKey = provider.env.map((item) => env[item]).find(Boolean)
       if (!apiKey) continue
       if (!apiKey) continue
-      mergeProvider(
-        providerID,
-        // only include apiKey if there's only one potential option
-        provider.env.length === 1 ? { apiKey } : {},
-        "env",
-      )
+      mergeProvider(providerID, {
+        source: "env",
+        key: provider.env.length === 1 ? apiKey : undefined,
+      })
     }
     }
 
 
     // load apikeys
     // load apikeys
     for (const [providerID, provider] of Object.entries(await Auth.all())) {
     for (const [providerID, provider] of Object.entries(await Auth.all())) {
       if (disabled.has(providerID)) continue
       if (disabled.has(providerID)) continue
       if (provider.type === "api") {
       if (provider.type === "api") {
-        mergeProvider(providerID, { apiKey: provider.key }, "api")
+        mergeProvider(providerID, {
+          source: "api",
+          key: provider.key,
+        })
       }
       }
     }
     }
 
 
@@ -485,7 +605,10 @@ export namespace Provider {
       // Load for the main provider if auth exists
       // Load for the main provider if auth exists
       if (auth) {
       if (auth) {
         const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
         const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
-        mergeProvider(plugin.auth.provider, options ?? {}, "custom")
+        mergeProvider(plugin.auth.provider, {
+          source: "custom",
+          options: options,
+        })
       }
       }
 
 
       // If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
       // If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
@@ -498,7 +621,10 @@ export namespace Provider {
               () => Auth.get(enterpriseProviderID) as any,
               () => Auth.get(enterpriseProviderID) as any,
               database[enterpriseProviderID],
               database[enterpriseProviderID],
             )
             )
-            mergeProvider(enterpriseProviderID, enterpriseOptions ?? {}, "custom")
+            mergeProvider(enterpriseProviderID, {
+              source: "custom",
+              options: enterpriseOptions,
+            })
           }
           }
         }
         }
       }
       }
@@ -508,13 +634,21 @@ export namespace Provider {
       if (disabled.has(providerID)) continue
       if (disabled.has(providerID)) continue
       const result = await fn(database[providerID])
       const result = await fn(database[providerID])
       if (result && (result.autoload || providers[providerID])) {
       if (result && (result.autoload || providers[providerID])) {
-        mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
+        if (result.getModel) modelLoaders[providerID] = result.getModel
+        mergeProvider(providerID, {
+          source: "custom",
+          options: result.options,
+        })
       }
       }
     }
     }
 
 
     // load config
     // load config
     for (const [providerID, provider] of configProviders) {
     for (const [providerID, provider] of configProviders) {
-      mergeProvider(providerID, provider.options ?? {}, "config")
+      const partial: Partial<Info> = { source: "config" }
+      if (provider.env) partial.env = provider.env
+      if (provider.name) partial.name = provider.name
+      if (provider.options) partial.options = provider.options
+      mergeProvider(providerID, partial)
     }
     }
 
 
     for (const [providerID, provider] of Object.entries(providers)) {
     for (const [providerID, provider] of Object.entries(providers)) {
@@ -524,49 +658,43 @@ export namespace Provider {
       }
       }
 
 
       if (providerID === "github-copilot" || providerID === "github-copilot-enterprise") {
       if (providerID === "github-copilot" || providerID === "github-copilot-enterprise") {
-        provider.info.npm = "@ai-sdk/github-copilot"
+        provider.models = mapValues(provider.models, (model) => ({
+          ...model,
+          api: {
+            ...model.api,
+            npm: "@ai-sdk/github-copilot",
+          },
+        }))
       }
       }
 
 
       const configProvider = config.provider?.[providerID]
       const configProvider = config.provider?.[providerID]
-      const filteredModels = Object.fromEntries(
-        Object.entries(provider.info.models)
-          // Filter out blacklisted models
-          .filter(
-            ([modelID]) =>
-              modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
-          )
-          // Filter out experimental models
-          .filter(
-            ([, model]) =>
-              ((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
-              model.status !== "deprecated",
-          )
-          // Filter by provider's whitelist/blacklist from config
-          .filter(([modelID]) => {
-            if (!configProvider) return true
-
-            return (
-              (!configProvider.blacklist || !configProvider.blacklist.includes(modelID)) &&
-              (!configProvider.whitelist || configProvider.whitelist.includes(modelID))
-            )
-          }),
-      )
 
 
-      provider.info.models = filteredModels
+      for (const [modelID, model] of Object.entries(provider.models)) {
+        model.api.id = model.api.id ?? model.id ?? modelID
+        if (modelID === "gpt-5-chat-latest" || (providerID === "openrouter" && modelID === "openai/gpt-5-chat"))
+          delete provider.models[modelID]
+        if ((model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) || model.status === "deprecated")
+          delete provider.models[modelID]
+        if (
+          (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
+          (configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
+        )
+          delete provider.models[modelID]
+      }
 
 
-      if (Object.keys(provider.info.models).length === 0) {
+      if (Object.keys(provider.models).length === 0) {
         delete providers[providerID]
         delete providers[providerID]
         continue
         continue
       }
       }
 
 
-      log.info("found", { providerID, npm: provider.info.npm })
+      log.info("found", { providerID })
     }
     }
 
 
     return {
     return {
-      models,
+      models: languages,
       providers,
       providers,
       sdk,
       sdk,
-      realIdByKey,
+      modelLoaders,
     }
     }
   })
   })
 
 
@@ -574,19 +702,28 @@ export namespace Provider {
     return state().then((state) => state.providers)
     return state().then((state) => state.providers)
   }
   }
 
 
-  async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model) {
-    return (async () => {
+  async function getSDK(model: Model) {
+    try {
       using _ = log.time("getSDK", {
       using _ = log.time("getSDK", {
-        providerID: provider.id,
+        providerID: model.providerID,
       })
       })
       const s = await state()
       const s = await state()
-      const pkg = model.provider?.npm ?? provider.npm ?? provider.id
-      const options = { ...s.providers[provider.id]?.options }
-      if (pkg.includes("@ai-sdk/openai-compatible") && options["includeUsage"] === undefined) {
+      const provider = s.providers[model.providerID]
+      const options = { ...provider.options }
+
+      if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
         options["includeUsage"] = true
         options["includeUsage"] = true
       }
       }
 
 
-      const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
+      if (!options["baseURL"]) options["baseURL"] = model.api.url
+      if (!options["apiKey"]) options["apiKey"] = provider.key
+      if (model.headers)
+        options["headers"] = {
+          ...options["headers"],
+          ...model.headers,
+        }
+
+      const key = Bun.hash.xxHash32(JSON.stringify({ npm: model.api.npm, options }))
       const existing = s.sdk.get(key)
       const existing = s.sdk.get(key)
       if (existing) return existing
       if (existing) return existing
 
 
@@ -615,12 +752,13 @@ export namespace Provider {
       }
       }
 
 
       // Special case: google-vertex-anthropic uses a subpath import
       // Special case: google-vertex-anthropic uses a subpath import
-      const bundledKey = provider.id === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : pkg
+      const bundledKey =
+        model.providerID === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : model.api.npm
       const bundledFn = BUNDLED_PROVIDERS[bundledKey]
       const bundledFn = BUNDLED_PROVIDERS[bundledKey]
       if (bundledFn) {
       if (bundledFn) {
-        log.info("using bundled provider", { providerID: provider.id, pkg: bundledKey })
+        log.info("using bundled provider", { providerID: model.providerID, pkg: bundledKey })
         const loaded = bundledFn({
         const loaded = bundledFn({
-          name: provider.id,
+          name: model.providerID,
           ...options,
           ...options,
         })
         })
         s.sdk.set(key, loaded)
         s.sdk.set(key, loaded)
@@ -628,25 +766,25 @@ export namespace Provider {
       }
       }
 
 
       let installedPath: string
       let installedPath: string
-      if (!pkg.startsWith("file://")) {
-        installedPath = await BunProc.install(pkg, "latest")
+      if (!model.api.npm.startsWith("file://")) {
+        installedPath = await BunProc.install(model.api.npm, "latest")
       } else {
       } else {
-        log.info("loading local provider", { pkg })
-        installedPath = pkg
+        log.info("loading local provider", { pkg: model.api.npm })
+        installedPath = model.api.npm
       }
       }
 
 
       const mod = await import(installedPath)
       const mod = await import(installedPath)
 
 
       const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
       const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
       const loaded = fn({
       const loaded = fn({
-        name: provider.id,
+        name: model.providerID,
         ...options,
         ...options,
       })
       })
       s.sdk.set(key, loaded)
       s.sdk.set(key, loaded)
       return loaded as SDK
       return loaded as SDK
-    })().catch((e) => {
-      throw new InitError({ providerID: provider.id }, { cause: e })
-    })
+    } catch (e) {
+      throw new InitError({ providerID: model.providerID }, { cause: e })
+    }
   }
   }
 
 
   export async function getProvider(providerID: string) {
   export async function getProvider(providerID: string) {
@@ -654,15 +792,7 @@ export namespace Provider {
   }
   }
 
 
   export async function getModel(providerID: string, modelID: string) {
   export async function getModel(providerID: string, modelID: string) {
-    const key = `${providerID}/${modelID}`
     const s = await state()
     const s = await state()
-    if (s.models.has(key)) return s.models.get(key)!
-
-    log.info("getModel", {
-      providerID,
-      modelID,
-    })
-
     const provider = s.providers[providerID]
     const provider = s.providers[providerID]
     if (!provider) {
     if (!provider) {
       const availableProviders = Object.keys(s.providers)
       const availableProviders = Object.keys(s.providers)
@@ -671,43 +801,36 @@ export namespace Provider {
       throw new ModelNotFoundError({ providerID, modelID, suggestions })
       throw new ModelNotFoundError({ providerID, modelID, suggestions })
     }
     }
 
 
-    const info = provider.info.models[modelID]
+    const info = provider.models[modelID]
     if (!info) {
     if (!info) {
-      const availableModels = Object.keys(provider.info.models)
+      const availableModels = Object.keys(provider.models)
       const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 })
       const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 })
       const suggestions = matches.map((m) => m.target)
       const suggestions = matches.map((m) => m.target)
       throw new ModelNotFoundError({ providerID, modelID, suggestions })
       throw new ModelNotFoundError({ providerID, modelID, suggestions })
     }
     }
+    return info
+  }
+
+  export async function getLanguage(model: Model) {
+    const s = await state()
+    const key = `${model.providerID}/${model.id}`
+    if (s.models.has(key)) return s.models.get(key)!
 
 
-    const sdk = await getSDK(provider.info, info)
+    const provider = s.providers[model.providerID]
+    const sdk = await getSDK(model)
 
 
     try {
     try {
-      const keyReal = `${providerID}/${modelID}`
-      const realID = s.realIdByKey.get(keyReal) ?? info.id
-      const language = provider.getModel
-        ? await provider.getModel(sdk, realID, provider.options)
-        : sdk.languageModel(realID)
-      log.info("found", { providerID, modelID })
-      s.models.set(key, {
-        providerID,
-        modelID,
-        info,
-        language,
-        npm: info.provider?.npm ?? provider.info.npm,
-      })
-      return {
-        modelID,
-        providerID,
-        info,
-        language,
-        npm: info.provider?.npm ?? provider.info.npm,
-      }
+      const language = s.modelLoaders[model.providerID]
+        ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
+        : sdk.languageModel(model.api.id)
+      s.models.set(key, language)
+      return language
     } catch (e) {
     } catch (e) {
       if (e instanceof NoSuchModelError)
       if (e instanceof NoSuchModelError)
         throw new ModelNotFoundError(
         throw new ModelNotFoundError(
           {
           {
-            modelID: modelID,
-            providerID,
+            modelID: model.id,
+            providerID: model.providerID,
           },
           },
           { cause: e },
           { cause: e },
         )
         )
@@ -720,7 +843,7 @@ export namespace Provider {
     const provider = s.providers[providerID]
     const provider = s.providers[providerID]
     if (!provider) return undefined
     if (!provider) return undefined
     for (const item of query) {
     for (const item of query) {
-      for (const modelID of Object.keys(provider.info.models)) {
+      for (const modelID of Object.keys(provider.models)) {
         if (modelID.includes(item))
         if (modelID.includes(item))
           return {
           return {
             providerID,
             providerID,
@@ -756,7 +879,7 @@ export namespace Provider {
         priority = ["gpt-5-nano"]
         priority = ["gpt-5-nano"]
       }
       }
       for (const item of priority) {
       for (const item of priority) {
-        for (const model of Object.keys(provider.info.models)) {
+        for (const model of Object.keys(provider.models)) {
           if (model.includes(item)) return getModel(providerID, model)
           if (model.includes(item)) return getModel(providerID, model)
         }
         }
       }
       }
@@ -764,7 +887,7 @@ export namespace Provider {
 
 
     // Check if opencode provider is available before using it
     // Check if opencode provider is available before using it
     const opencodeProvider = await state().then((state) => state.providers["opencode"])
     const opencodeProvider = await state().then((state) => state.providers["opencode"])
-    if (opencodeProvider && opencodeProvider.info.models["gpt-5-nano"]) {
+    if (opencodeProvider && opencodeProvider.models["gpt-5-nano"]) {
       return getModel("opencode", "gpt-5-nano")
       return getModel("opencode", "gpt-5-nano")
     }
     }
 
 
@@ -772,7 +895,7 @@ export namespace Provider {
   }
   }
 
 
   const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
   const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
-  export function sort(models: ModelsDev.Model[]) {
+  export function sort(models: Model[]) {
     return sortBy(
     return sortBy(
       models,
       models,
       [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
       [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
@@ -787,12 +910,12 @@ export namespace Provider {
 
 
     const provider = await list()
     const provider = await list()
       .then((val) => Object.values(val))
       .then((val) => Object.values(val))
-      .then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
+      .then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)))
     if (!provider) throw new Error("no providers found")
     if (!provider) throw new Error("no providers found")
-    const [model] = sort(Object.values(provider.info.models))
+    const [model] = sort(Object.values(provider.models))
     if (!model) throw new Error("no models found")
     if (!model) throw new Error("no models found")
     return {
     return {
-      providerID: provider.info.id,
+      providerID: provider.id,
       modelID: model.id,
       modelID: model.id,
     }
     }
   }
   }

+ 32 - 30
packages/opencode/src/provider/transform.ts

@@ -1,10 +1,11 @@
 import type { APICallError, ModelMessage } from "ai"
 import type { APICallError, ModelMessage } from "ai"
 import { unique } from "remeda"
 import { unique } from "remeda"
 import type { JSONSchema } from "zod/v4/core"
 import type { JSONSchema } from "zod/v4/core"
+import type { Provider } from "./provider"
 
 
 export namespace ProviderTransform {
 export namespace ProviderTransform {
-  function normalizeMessages(msgs: ModelMessage[], providerID: string, modelID: string): ModelMessage[] {
-    if (modelID.includes("claude")) {
+  function normalizeMessages(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
+    if (model.api.id.includes("claude")) {
       return msgs.map((msg) => {
       return msgs.map((msg) => {
         if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
         if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
           msg.content = msg.content.map((part) => {
           msg.content = msg.content.map((part) => {
@@ -20,7 +21,7 @@ export namespace ProviderTransform {
         return msg
         return msg
       })
       })
     }
     }
-    if (providerID === "mistral" || modelID.toLowerCase().includes("mistral")) {
+    if (model.providerID === "mistral" || model.api.id.toLowerCase().includes("mistral")) {
       const result: ModelMessage[] = []
       const result: ModelMessage[] = []
       for (let i = 0; i < msgs.length; i++) {
       for (let i = 0; i < msgs.length; i++) {
         const msg = msgs[i]
         const msg = msgs[i]
@@ -107,67 +108,68 @@ export namespace ProviderTransform {
     return msgs
     return msgs
   }
   }
 
 
-  export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
-    msgs = normalizeMessages(msgs, providerID, modelID)
-    if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
-      msgs = applyCaching(msgs, providerID)
+  export function message(msgs: ModelMessage[], model: Provider.Model) {
+    msgs = normalizeMessages(msgs, model)
+    if (model.providerID === "anthropic" || model.api.id.includes("anthropic") || model.api.id.includes("claude")) {
+      msgs = applyCaching(msgs, model.providerID)
     }
     }
 
 
     return msgs
     return msgs
   }
   }
 
 
-  export function temperature(_providerID: string, modelID: string) {
-    if (modelID.toLowerCase().includes("qwen")) return 0.55
-    if (modelID.toLowerCase().includes("claude")) return undefined
-    if (modelID.toLowerCase().includes("gemini-3-pro")) return 1.0
+  export function temperature(model: Provider.Model) {
+    if (model.api.id.toLowerCase().includes("qwen")) return 0.55
+    if (model.api.id.toLowerCase().includes("claude")) return undefined
+    if (model.api.id.toLowerCase().includes("gemini-3-pro")) return 1.0
     return 0
     return 0
   }
   }
 
 
-  export function topP(_providerID: string, modelID: string) {
-    if (modelID.toLowerCase().includes("qwen")) return 1
+  export function topP(model: Provider.Model) {
+    if (model.api.id.toLowerCase().includes("qwen")) return 1
     return undefined
     return undefined
   }
   }
 
 
   export function options(
   export function options(
-    providerID: string,
-    modelID: string,
-    npm: string,
+    model: Provider.Model,
     sessionID: string,
     sessionID: string,
     providerOptions?: Record<string, any>,
     providerOptions?: Record<string, any>,
   ): Record<string, any> {
   ): Record<string, any> {
     const result: Record<string, any> = {}
     const result: Record<string, any> = {}
 
 
     // switch to providerID later, for now use this
     // switch to providerID later, for now use this
-    if (npm === "@openrouter/ai-sdk-provider") {
+    if (model.api.npm === "@openrouter/ai-sdk-provider") {
       result["usage"] = {
       result["usage"] = {
         include: true,
         include: true,
       }
       }
     }
     }
 
 
-    if (providerID === "openai" || providerOptions?.setCacheKey) {
+    if (model.providerID === "openai" || providerOptions?.setCacheKey) {
       result["promptCacheKey"] = sessionID
       result["promptCacheKey"] = sessionID
     }
     }
 
 
-    if (providerID === "google" || (providerID.startsWith("opencode") && modelID.includes("gemini-3"))) {
+    if (
+      model.providerID === "google" ||
+      (model.providerID.startsWith("opencode") && model.api.id.includes("gemini-3"))
+    ) {
       result["thinkingConfig"] = {
       result["thinkingConfig"] = {
         includeThoughts: true,
         includeThoughts: true,
       }
       }
     }
     }
 
 
-    if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
-      if (modelID.includes("codex")) {
+    if (model.providerID.includes("gpt-5") && !model.api.id.includes("gpt-5-chat")) {
+      if (model.providerID.includes("codex")) {
         result["store"] = false
         result["store"] = false
       }
       }
 
 
-      if (!modelID.includes("codex") && !modelID.includes("gpt-5-pro")) {
+      if (!model.api.id.includes("codex") && !model.api.id.includes("gpt-5-pro")) {
         result["reasoningEffort"] = "medium"
         result["reasoningEffort"] = "medium"
       }
       }
 
 
-      if (modelID.endsWith("gpt-5.1") && providerID !== "azure") {
+      if (model.api.id.endsWith("gpt-5.1") && model.providerID !== "azure") {
         result["textVerbosity"] = "low"
         result["textVerbosity"] = "low"
       }
       }
 
 
-      if (providerID.startsWith("opencode")) {
+      if (model.providerID.startsWith("opencode")) {
         result["promptCacheKey"] = sessionID
         result["promptCacheKey"] = sessionID
         result["include"] = ["reasoning.encrypted_content"]
         result["include"] = ["reasoning.encrypted_content"]
         result["reasoningSummary"] = "auto"
         result["reasoningSummary"] = "auto"
@@ -176,17 +178,17 @@ export namespace ProviderTransform {
     return result
     return result
   }
   }
 
 
-  export function smallOptions(input: { providerID: string; modelID: string }) {
+  export function smallOptions(model: Provider.Model) {
     const options: Record<string, any> = {}
     const options: Record<string, any> = {}
 
 
-    if (input.providerID === "openai" || input.modelID.includes("gpt-5")) {
-      if (input.modelID.includes("5.1")) {
+    if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
+      if (model.api.id.includes("5.1")) {
         options["reasoningEffort"] = "low"
         options["reasoningEffort"] = "low"
       } else {
       } else {
         options["reasoningEffort"] = "minimal"
         options["reasoningEffort"] = "minimal"
       }
       }
     }
     }
-    if (input.providerID === "google") {
+    if (model.providerID === "google") {
       options["thinkingConfig"] = {
       options["thinkingConfig"] = {
         thinkingBudget: 0,
         thinkingBudget: 0,
       }
       }
@@ -254,7 +256,7 @@ export namespace ProviderTransform {
     return standardLimit
     return standardLimit
   }
   }
 
 
-  export function schema(providerID: string, modelID: string, schema: JSONSchema.BaseSchema) {
+  export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema) {
     /*
     /*
     if (["openai", "azure"].includes(providerID)) {
     if (["openai", "azure"].includes(providerID)) {
       if (schema.type === "object" && schema.properties) {
       if (schema.type === "object" && schema.properties) {
@@ -274,7 +276,7 @@ export namespace ProviderTransform {
     */
     */
 
 
     // Convert integer enums to string enums for Google/Gemini
     // Convert integer enums to string enums for Google/Gemini
-    if (providerID === "google" || modelID.includes("gemini")) {
+    if (model.providerID === "google" || model.api.id.includes("gemini")) {
       const sanitizeGemini = (obj: any): any => {
       const sanitizeGemini = (obj: any): any => {
         if (obj === null || typeof obj !== "object") {
         if (obj === null || typeof obj !== "object") {
           return obj
           return obj

+ 10 - 7
packages/opencode/src/server/server.ts

@@ -8,7 +8,7 @@ import { proxy } from "hono/proxy"
 import { Session } from "../session"
 import { Session } from "../session"
 import z from "zod"
 import z from "zod"
 import { Provider } from "../provider/provider"
 import { Provider } from "../provider/provider"
-import { mapValues } from "remeda"
+import { mapValues, pipe } from "remeda"
 import { NamedError } from "@opencode-ai/util/error"
 import { NamedError } from "@opencode-ai/util/error"
 import { ModelsDev } from "../provider/models"
 import { ModelsDev } from "../provider/models"
 import { Ripgrep } from "../file/ripgrep"
 import { Ripgrep } from "../file/ripgrep"
@@ -296,8 +296,8 @@ export namespace Server {
           }),
           }),
         ),
         ),
         async (c) => {
         async (c) => {
-          const { provider, model } = c.req.valid("query")
-          const tools = await ToolRegistry.tools(provider, model)
+          const { provider } = c.req.valid("query")
+          const tools = await ToolRegistry.tools(provider)
           return c.json(
           return c.json(
             tools.map((t) => ({
             tools.map((t) => ({
               id: t.id,
               id: t.id,
@@ -1025,7 +1025,7 @@ export namespace Server {
         async (c) => {
         async (c) => {
           c.status(204)
           c.status(204)
           c.header("Content-Type", "application/json")
           c.header("Content-Type", "application/json")
-          return stream(c, async (stream) => {
+          return stream(c, async () => {
             const sessionID = c.req.valid("param").id
             const sessionID = c.req.valid("param").id
             const body = c.req.valid("json")
             const body = c.req.valid("json")
             SessionPrompt.prompt({ ...body, sessionID })
             SessionPrompt.prompt({ ...body, sessionID })
@@ -1231,7 +1231,7 @@ export namespace Server {
                 "application/json": {
                 "application/json": {
                   schema: resolver(
                   schema: resolver(
                     z.object({
                     z.object({
-                      providers: ModelsDev.Provider.array(),
+                      providers: Provider.Info.array(),
                       default: z.record(z.string(), z.string()),
                       default: z.record(z.string(), z.string()),
                     }),
                     }),
                   ),
                   ),
@@ -1242,7 +1242,7 @@ export namespace Server {
         }),
         }),
         async (c) => {
         async (c) => {
           using _ = log.time("providers")
           using _ = log.time("providers")
-          const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
+          const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
           return c.json({
           return c.json({
             providers: Object.values(providers),
             providers: Object.values(providers),
             default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
             default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
@@ -1272,7 +1272,10 @@ export namespace Server {
           },
           },
         }),
         }),
         async (c) => {
         async (c) => {
-          const providers = await ModelsDev.get()
+          const providers = pipe(
+            await ModelsDev.get(),
+            mapValues((x) => Provider.fromModelsDevProvider(x)),
+          )
           const connected = await Provider.list().then((x) => Object.keys(x))
           const connected = await Provider.list().then((x) => Object.keys(x))
           return c.json({
           return c.json({
             all: Object.values(providers),
             all: Object.values(providers),

+ 61 - 68
packages/opencode/src/session/compaction.ts

@@ -1,4 +1,4 @@
-import { streamText, wrapLanguageModel, type ModelMessage } from "ai"
+import { wrapLanguageModel, type ModelMessage } from "ai"
 import { Session } from "."
 import { Session } from "."
 import { Identifier } from "../id/id"
 import { Identifier } from "../id/id"
 import { Instance } from "../project/instance"
 import { Instance } from "../project/instance"
@@ -7,7 +7,6 @@ import { MessageV2 } from "./message-v2"
 import { SystemPrompt } from "./system"
 import { SystemPrompt } from "./system"
 import { Bus } from "../bus"
 import { Bus } from "../bus"
 import z from "zod"
 import z from "zod"
-import type { ModelsDev } from "../provider/models"
 import { SessionPrompt } from "./prompt"
 import { SessionPrompt } from "./prompt"
 import { Flag } from "../flag/flag"
 import { Flag } from "../flag/flag"
 import { Token } from "../util/token"
 import { Token } from "../util/token"
@@ -29,7 +28,7 @@ export namespace SessionCompaction {
     ),
     ),
   }
   }
 
 
-  export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: ModelsDev.Model }) {
+  export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) {
     if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) return false
     if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) return false
     const context = input.model.limit.context
     const context = input.model.limit.context
     if (context === 0) return false
     if (context === 0) return false
@@ -98,6 +97,7 @@ export namespace SessionCompaction {
     auto: boolean
     auto: boolean
   }) {
   }) {
     const model = await Provider.getModel(input.model.providerID, input.model.modelID)
     const model = await Provider.getModel(input.model.providerID, input.model.modelID)
+    const language = await Provider.getLanguage(model)
     const system = [...SystemPrompt.compaction(model.providerID)]
     const system = [...SystemPrompt.compaction(model.providerID)]
     const msg = (await Session.updateMessage({
     const msg = (await Session.updateMessage({
       id: Identifier.ascending("message"),
       id: Identifier.ascending("message"),
@@ -126,79 +126,72 @@ export namespace SessionCompaction {
     const processor = SessionProcessor.create({
     const processor = SessionProcessor.create({
       assistantMessage: msg,
       assistantMessage: msg,
       sessionID: input.sessionID,
       sessionID: input.sessionID,
-      providerID: input.model.providerID,
-      model: model.info,
+      model: model,
       abort: input.abort,
       abort: input.abort,
     })
     })
-    const result = await processor.process(() =>
-      streamText({
-        onError(error) {
-          log.error("stream error", {
-            error,
-          })
-        },
-        // set to 0, we handle loop
-        maxRetries: 0,
-        providerOptions: ProviderTransform.providerOptions(
-          model.npm,
-          model.providerID,
-          pipe(
-            {},
-            mergeDeep(ProviderTransform.options(model.providerID, model.modelID, model.npm ?? "", input.sessionID)),
-            mergeDeep(model.info.options),
-          ),
+    const result = await processor.process({
+      onError(error) {
+        log.error("stream error", {
+          error,
+        })
+      },
+      // set to 0, we handle loop
+      maxRetries: 0,
+      providerOptions: ProviderTransform.providerOptions(
+        model.api.npm,
+        model.providerID,
+        pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
+      ),
+      headers: model.headers,
+      abortSignal: input.abort,
+      tools: model.capabilities.toolcall ? {} : undefined,
+      messages: [
+        ...system.map(
+          (x): ModelMessage => ({
+            role: "system",
+            content: x,
+          }),
         ),
         ),
-        headers: model.info.headers,
-        abortSignal: input.abort,
-        tools: model.info.tool_call ? {} : undefined,
-        messages: [
-          ...system.map(
-            (x): ModelMessage => ({
-              role: "system",
-              content: x,
-            }),
-          ),
-          ...MessageV2.toModelMessage(
-            input.messages.filter((m) => {
-              if (m.info.role !== "assistant" || m.info.error === undefined) {
-                return true
-              }
-              if (
-                MessageV2.AbortedError.isInstance(m.info.error) &&
-                m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
-              ) {
-                return true
-              }
+        ...MessageV2.toModelMessage(
+          input.messages.filter((m) => {
+            if (m.info.role !== "assistant" || m.info.error === undefined) {
+              return true
+            }
+            if (
+              MessageV2.AbortedError.isInstance(m.info.error) &&
+              m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
+            ) {
+              return true
+            }
 
 
-              return false
-            }),
-          ),
-          {
-            role: "user",
-            content: [
-              {
-                type: "text",
-                text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.",
-              },
-            ],
-          },
-        ],
-        model: wrapLanguageModel({
-          model: model.language,
-          middleware: [
+            return false
+          }),
+        ),
+        {
+          role: "user",
+          content: [
             {
             {
-              async transformParams(args) {
-                if (args.type === "stream") {
-                  // @ts-expect-error
-                  args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
-                }
-                return args.params
-              },
+              type: "text",
+              text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.",
             },
             },
           ],
           ],
-        }),
+        },
+      ],
+      model: wrapLanguageModel({
+        model: language,
+        middleware: [
+          {
+            async transformParams(args) {
+              if (args.type === "stream") {
+                // @ts-expect-error
+                args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
+              }
+              return args.params
+            },
+          },
+        ],
       }),
       }),
-    )
+    })
     if (result === "continue" && input.auto) {
     if (result === "continue" && input.auto) {
       const continueMsg = await Session.updateMessage({
       const continueMsg = await Session.updateMessage({
         id: Identifier.ascending("message"),
         id: Identifier.ascending("message"),

+ 12 - 8
packages/opencode/src/session/index.ts

@@ -6,8 +6,7 @@ import { Config } from "../config/config"
 import { Flag } from "../flag/flag"
 import { Flag } from "../flag/flag"
 import { Identifier } from "../id/id"
 import { Identifier } from "../id/id"
 import { Installation } from "../installation"
 import { Installation } from "../installation"
-import type { ModelsDev } from "../provider/models"
-import { Share } from "../share/share"
+
 import { Storage } from "../storage/storage"
 import { Storage } from "../storage/storage"
 import { Log } from "../util/log"
 import { Log } from "../util/log"
 import { MessageV2 } from "./message-v2"
 import { MessageV2 } from "./message-v2"
@@ -16,7 +15,8 @@ import { SessionPrompt } from "./prompt"
 import { fn } from "@/util/fn"
 import { fn } from "@/util/fn"
 import { Command } from "../command"
 import { Command } from "../command"
 import { Snapshot } from "@/snapshot"
 import { Snapshot } from "@/snapshot"
-import { ShareNext } from "@/share/share-next"
+
+import type { Provider } from "@/provider/provider"
 
 
 export namespace Session {
 export namespace Session {
   const log = Log.create({ service: "session" })
   const log = Log.create({ service: "session" })
@@ -223,6 +223,7 @@ export namespace Session {
     }
     }
 
 
     if (cfg.enterprise?.url) {
     if (cfg.enterprise?.url) {
+      const { ShareNext } = await import("@/share/share-next")
       const share = await ShareNext.create(id)
       const share = await ShareNext.create(id)
       await update(id, (draft) => {
       await update(id, (draft) => {
         draft.share = {
         draft.share = {
@@ -233,6 +234,7 @@ export namespace Session {
 
 
     const session = await get(id)
     const session = await get(id)
     if (session.share) return session.share
     if (session.share) return session.share
+    const { Share } = await import("../share/share")
     const share = await Share.create(id)
     const share = await Share.create(id)
     await update(id, (draft) => {
     await update(id, (draft) => {
       draft.share = {
       draft.share = {
@@ -253,6 +255,7 @@ export namespace Session {
   export const unshare = fn(Identifier.schema("session"), async (id) => {
   export const unshare = fn(Identifier.schema("session"), async (id) => {
     const cfg = await Config.get()
     const cfg = await Config.get()
     if (cfg.enterprise?.url) {
     if (cfg.enterprise?.url) {
+      const { ShareNext } = await import("@/share/share-next")
       await ShareNext.remove(id)
       await ShareNext.remove(id)
       await update(id, (draft) => {
       await update(id, (draft) => {
         draft.share = undefined
         draft.share = undefined
@@ -264,6 +267,7 @@ export namespace Session {
     await update(id, (draft) => {
     await update(id, (draft) => {
       draft.share = undefined
       draft.share = undefined
     })
     })
+    const { Share } = await import("../share/share")
     await Share.remove(id, share.secret)
     await Share.remove(id, share.secret)
   })
   })
 
 
@@ -389,7 +393,7 @@ export namespace Session {
 
 
   export const getUsage = fn(
   export const getUsage = fn(
     z.object({
     z.object({
-      model: z.custom<ModelsDev.Model>(),
+      model: z.custom<Provider.Model>(),
       usage: z.custom<LanguageModelUsage>(),
       usage: z.custom<LanguageModelUsage>(),
       metadata: z.custom<ProviderMetadata>().optional(),
       metadata: z.custom<ProviderMetadata>().optional(),
     }),
     }),
@@ -420,16 +424,16 @@ export namespace Session {
       }
       }
 
 
       const costInfo =
       const costInfo =
-        input.model.cost?.context_over_200k && tokens.input + tokens.cache.read > 200_000
-          ? input.model.cost.context_over_200k
+        input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
+          ? input.model.cost.experimentalOver200K
           : input.model.cost
           : input.model.cost
       return {
       return {
         cost: safe(
         cost: safe(
           new Decimal(0)
           new Decimal(0)
             .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
             .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
             .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
             .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
-            .add(new Decimal(tokens.cache.read).mul(costInfo?.cache_read ?? 0).div(1_000_000))
-            .add(new Decimal(tokens.cache.write).mul(costInfo?.cache_write ?? 0).div(1_000_000))
+            .add(new Decimal(tokens.cache.read).mul(costInfo?.cache.read ?? 0).div(1_000_000))
+            .add(new Decimal(tokens.cache.write).mul(costInfo?.cache.write ?? 0).div(1_000_000))
             // TODO: update models.dev to have better pricing model, for now:
             // TODO: update models.dev to have better pricing model, for now:
             // charge reasoning tokens at the same rate as output tokens
             // charge reasoning tokens at the same rate as output tokens
             .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
             .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))

+ 17 - 8
packages/opencode/src/session/processor.ts

@@ -1,6 +1,5 @@
-import type { ModelsDev } from "@/provider/models"
 import { MessageV2 } from "./message-v2"
 import { MessageV2 } from "./message-v2"
-import { type StreamTextResult, type Tool as AITool, APICallError } from "ai"
+import { streamText } from "ai"
 import { Log } from "@/util/log"
 import { Log } from "@/util/log"
 import { Identifier } from "@/id/id"
 import { Identifier } from "@/id/id"
 import { Session } from "."
 import { Session } from "."
@@ -11,6 +10,7 @@ import { SessionSummary } from "./summary"
 import { Bus } from "@/bus"
 import { Bus } from "@/bus"
 import { SessionRetry } from "./retry"
 import { SessionRetry } from "./retry"
 import { SessionStatus } from "./status"
 import { SessionStatus } from "./status"
+import type { Provider } from "@/provider/provider"
 
 
 export namespace SessionProcessor {
 export namespace SessionProcessor {
   const DOOM_LOOP_THRESHOLD = 3
   const DOOM_LOOP_THRESHOLD = 3
@@ -19,11 +19,19 @@ export namespace SessionProcessor {
   export type Info = Awaited<ReturnType<typeof create>>
   export type Info = Awaited<ReturnType<typeof create>>
   export type Result = Awaited<ReturnType<Info["process"]>>
   export type Result = Awaited<ReturnType<Info["process"]>>
 
 
+  export type StreamInput = Parameters<typeof streamText>[0]
+
+  export type TBD = {
+    model: {
+      modelID: string
+      providerID: string
+    }
+  }
+
   export function create(input: {
   export function create(input: {
     assistantMessage: MessageV2.Assistant
     assistantMessage: MessageV2.Assistant
     sessionID: string
     sessionID: string
-    providerID: string
-    model: ModelsDev.Model
+    model: Provider.Model
     abort: AbortSignal
     abort: AbortSignal
   }) {
   }) {
     const toolcalls: Record<string, MessageV2.ToolPart> = {}
     const toolcalls: Record<string, MessageV2.ToolPart> = {}
@@ -38,13 +46,13 @@ export namespace SessionProcessor {
       partFromToolCall(toolCallID: string) {
       partFromToolCall(toolCallID: string) {
         return toolcalls[toolCallID]
         return toolcalls[toolCallID]
       },
       },
-      async process(fn: () => StreamTextResult<Record<string, AITool>, never>) {
+      async process(streamInput: StreamInput) {
         log.info("process")
         log.info("process")
         while (true) {
         while (true) {
           try {
           try {
             let currentText: MessageV2.TextPart | undefined
             let currentText: MessageV2.TextPart | undefined
             let reasoningMap: Record<string, MessageV2.ReasoningPart> = {}
             let reasoningMap: Record<string, MessageV2.ReasoningPart> = {}
-            const stream = fn()
+            const stream = streamText(streamInput)
 
 
             for await (const value of stream.fullStream) {
             for await (const value of stream.fullStream) {
               input.abort.throwIfAborted()
               input.abort.throwIfAborted()
@@ -328,11 +336,12 @@ export namespace SessionProcessor {
                   continue
                   continue
               }
               }
             }
             }
-          } catch (e) {
+          } catch (e: any) {
             log.error("process", {
             log.error("process", {
               error: e,
               error: e,
+              stack: JSON.stringify(e.stack),
             })
             })
-            const error = MessageV2.fromError(e, { providerID: input.providerID })
+            const error = MessageV2.fromError(e, { providerID: input.sessionID })
             const retry = SessionRetry.retryable(error)
             const retry = SessionRetry.retryable(error)
             if (retry !== undefined) {
             if (retry !== undefined) {
               attempt++
               attempt++

+ 126 - 151
packages/opencode/src/session/prompt.ts

@@ -11,7 +11,6 @@ import { Agent } from "../agent/agent"
 import { Provider } from "../provider/provider"
 import { Provider } from "../provider/provider"
 import {
 import {
   generateText,
   generateText,
-  streamText,
   type ModelMessage,
   type ModelMessage,
   type Tool as AITool,
   type Tool as AITool,
   tool,
   tool,
@@ -288,6 +287,7 @@ export namespace SessionPrompt {
         })
         })
 
 
       const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
       const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
+      const language = await Provider.getLanguage(model)
       const task = tasks.pop()
       const task = tasks.pop()
 
 
       // pending subtask
       // pending subtask
@@ -311,7 +311,7 @@ export namespace SessionPrompt {
             reasoning: 0,
             reasoning: 0,
             cache: { read: 0, write: 0 },
             cache: { read: 0, write: 0 },
           },
           },
-          modelID: model.modelID,
+          modelID: model.id,
           providerID: model.providerID,
           providerID: model.providerID,
           time: {
           time: {
             created: Date.now(),
             created: Date.now(),
@@ -408,7 +408,7 @@ export namespace SessionPrompt {
           agent: lastUser.agent,
           agent: lastUser.agent,
           model: {
           model: {
             providerID: model.providerID,
             providerID: model.providerID,
-            modelID: model.modelID,
+            modelID: model.id,
           },
           },
           sessionID,
           sessionID,
           auto: task.auto,
           auto: task.auto,
@@ -421,7 +421,7 @@ export namespace SessionPrompt {
       if (
       if (
         lastFinished &&
         lastFinished &&
         lastFinished.summary !== true &&
         lastFinished.summary !== true &&
-        SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model: model.info })
+        SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })
       ) {
       ) {
         await SessionCompaction.create({
         await SessionCompaction.create({
           sessionID,
           sessionID,
@@ -455,7 +455,7 @@ export namespace SessionPrompt {
             reasoning: 0,
             reasoning: 0,
             cache: { read: 0, write: 0 },
             cache: { read: 0, write: 0 },
           },
           },
-          modelID: model.modelID,
+          modelID: model.id,
           providerID: model.providerID,
           providerID: model.providerID,
           time: {
           time: {
             created: Date.now(),
             created: Date.now(),
@@ -463,20 +463,18 @@ export namespace SessionPrompt {
           sessionID,
           sessionID,
         })) as MessageV2.Assistant,
         })) as MessageV2.Assistant,
         sessionID: sessionID,
         sessionID: sessionID,
-        model: model.info,
-        providerID: model.providerID,
+        model,
         abort,
         abort,
       })
       })
       const system = await resolveSystemPrompt({
       const system = await resolveSystemPrompt({
-        providerID: model.providerID,
-        modelID: model.info.id,
+        model,
         agent,
         agent,
         system: lastUser.system,
         system: lastUser.system,
       })
       })
       const tools = await resolveTools({
       const tools = await resolveTools({
         agent,
         agent,
         sessionID,
         sessionID,
-        model: lastUser.model,
+        model,
         tools: lastUser.tools,
         tools: lastUser.tools,
         processor,
         processor,
       })
       })
@@ -486,21 +484,19 @@ export namespace SessionPrompt {
         {
         {
           sessionID: sessionID,
           sessionID: sessionID,
           agent: lastUser.agent,
           agent: lastUser.agent,
-          model: model.info,
+          model: model,
           provider,
           provider,
           message: lastUser,
           message: lastUser,
         },
         },
         {
         {
-          temperature: model.info.temperature
-            ? (agent.temperature ?? ProviderTransform.temperature(model.providerID, model.modelID))
+          temperature: model.capabilities.temperature
+            ? (agent.temperature ?? ProviderTransform.temperature(model))
             : undefined,
             : undefined,
-          topP: agent.topP ?? ProviderTransform.topP(model.providerID, model.modelID),
+          topP: agent.topP ?? ProviderTransform.topP(model),
           options: pipe(
           options: pipe(
             {},
             {},
-            mergeDeep(
-              ProviderTransform.options(model.providerID, model.modelID, model.npm ?? "", sessionID, provider?.options),
-            ),
-            mergeDeep(model.info.options),
+            mergeDeep(ProviderTransform.options(model, sessionID, provider?.options)),
+            mergeDeep(model.options),
             mergeDeep(agent.options),
             mergeDeep(agent.options),
           ),
           ),
         },
         },
@@ -513,113 +509,111 @@ export namespace SessionPrompt {
         })
         })
       }
       }
 
 
-      const result = await processor.process(() =>
-        streamText({
-          onError(error) {
-            log.error("stream error", {
-              error,
+      const result = await processor.process({
+        onError(error) {
+          log.error("stream error", {
+            error,
+          })
+        },
+        async experimental_repairToolCall(input) {
+          const lower = input.toolCall.toolName.toLowerCase()
+          if (lower !== input.toolCall.toolName && tools[lower]) {
+            log.info("repairing tool call", {
+              tool: input.toolCall.toolName,
+              repaired: lower,
             })
             })
-          },
-          async experimental_repairToolCall(input) {
-            const lower = input.toolCall.toolName.toLowerCase()
-            if (lower !== input.toolCall.toolName && tools[lower]) {
-              log.info("repairing tool call", {
-                tool: input.toolCall.toolName,
-                repaired: lower,
-              })
-              return {
-                ...input.toolCall,
-                toolName: lower,
-              }
-            }
             return {
             return {
               ...input.toolCall,
               ...input.toolCall,
-              input: JSON.stringify({
-                tool: input.toolCall.toolName,
-                error: input.error.message,
-              }),
-              toolName: "invalid",
+              toolName: lower,
             }
             }
-          },
-          headers: {
-            ...(model.providerID.startsWith("opencode")
-              ? {
-                  "x-opencode-project": Instance.project.id,
-                  "x-opencode-session": sessionID,
-                  "x-opencode-request": lastUser.id,
-                }
-              : undefined),
-            ...model.info.headers,
-          },
-          // set to 0, we handle loop
-          maxRetries: 0,
-          activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
-          maxOutputTokens: ProviderTransform.maxOutputTokens(
-            model.providerID,
-            params.options,
-            model.info.limit.output,
-            OUTPUT_TOKEN_MAX,
+          }
+          return {
+            ...input.toolCall,
+            input: JSON.stringify({
+              tool: input.toolCall.toolName,
+              error: input.error.message,
+            }),
+            toolName: "invalid",
+          }
+        },
+        headers: {
+          ...(model.providerID.startsWith("opencode")
+            ? {
+                "x-opencode-project": Instance.project.id,
+                "x-opencode-session": sessionID,
+                "x-opencode-request": lastUser.id,
+              }
+            : undefined),
+          ...model.headers,
+        },
+        // set to 0, we handle loop
+        maxRetries: 0,
+        activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
+        maxOutputTokens: ProviderTransform.maxOutputTokens(
+          model.api.npm,
+          params.options,
+          model.limit.output,
+          OUTPUT_TOKEN_MAX,
+        ),
+        abortSignal: abort,
+        providerOptions: ProviderTransform.providerOptions(model.api.npm, model.providerID, params.options),
+        stopWhen: stepCountIs(1),
+        temperature: params.temperature,
+        topP: params.topP,
+        messages: [
+          ...system.map(
+            (x): ModelMessage => ({
+              role: "system",
+              content: x,
+            }),
           ),
           ),
-          abortSignal: abort,
-          providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
-          stopWhen: stepCountIs(1),
-          temperature: params.temperature,
-          topP: params.topP,
-          messages: [
-            ...system.map(
-              (x): ModelMessage => ({
-                role: "system",
-                content: x,
-              }),
-            ),
-            ...MessageV2.toModelMessage(
-              msgs.filter((m) => {
-                if (m.info.role !== "assistant" || m.info.error === undefined) {
-                  return true
-                }
-                if (
-                  MessageV2.AbortedError.isInstance(m.info.error) &&
-                  m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
-                ) {
-                  return true
-                }
+          ...MessageV2.toModelMessage(
+            msgs.filter((m) => {
+              if (m.info.role !== "assistant" || m.info.error === undefined) {
+                return true
+              }
+              if (
+                MessageV2.AbortedError.isInstance(m.info.error) &&
+                m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
+              ) {
+                return true
+              }
 
 
-                return false
-              }),
-            ),
-          ],
-          tools: model.info.tool_call === false ? undefined : tools,
-          model: wrapLanguageModel({
-            model: model.language,
-            middleware: [
-              {
-                async transformParams(args) {
-                  if (args.type === "stream") {
-                    // @ts-expect-error
-                    args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
-                  }
-                  // Transform tool schemas for provider compatibility
-                  if (args.params.tools && Array.isArray(args.params.tools)) {
-                    args.params.tools = args.params.tools.map((tool: any) => {
-                      // Tools at middleware level have inputSchema, not parameters
-                      if (tool.inputSchema && typeof tool.inputSchema === "object") {
-                        // Transform the inputSchema for provider compatibility
-                        return {
-                          ...tool,
-                          inputSchema: ProviderTransform.schema(model.providerID, model.modelID, tool.inputSchema),
-                        }
+              return false
+            }),
+          ),
+        ],
+        tools: model.capabilities.toolcall === false ? undefined : tools,
+        model: wrapLanguageModel({
+          model: language,
+          middleware: [
+            {
+              async transformParams(args) {
+                if (args.type === "stream") {
+                  // @ts-expect-error - prompt types are compatible at runtime
+                  args.params.prompt = ProviderTransform.message(args.params.prompt, model)
+                }
+                // Transform tool schemas for provider compatibility
+                if (args.params.tools && Array.isArray(args.params.tools)) {
+                  args.params.tools = args.params.tools.map((tool: any) => {
+                    // Tools at middleware level have inputSchema, not parameters
+                    if (tool.inputSchema && typeof tool.inputSchema === "object") {
+                      // Transform the inputSchema for provider compatibility
+                      return {
+                        ...tool,
+                        inputSchema: ProviderTransform.schema(model, tool.inputSchema),
                       }
                       }
-                      // If no inputSchema, return tool unchanged
-                      return tool
-                    })
-                  }
-                  return args.params
-                },
+                    }
+                    // If no inputSchema, return tool unchanged
+                    return tool
+                  })
+                }
+                return args.params
               },
               },
-            ],
-          }),
+            },
+          ],
         }),
         }),
-      )
+      })
       if (result === "stop") break
       if (result === "stop") break
       continue
       continue
     }
     }
@@ -642,18 +636,13 @@ export namespace SessionPrompt {
     return Provider.defaultModel()
     return Provider.defaultModel()
   }
   }
 
 
-  async function resolveSystemPrompt(input: {
-    system?: string
-    agent: Agent.Info
-    providerID: string
-    modelID: string
-  }) {
-    let system = SystemPrompt.header(input.providerID)
+  async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) {
+    let system = SystemPrompt.header(input.model.providerID)
     system.push(
     system.push(
       ...(() => {
       ...(() => {
         if (input.system) return [input.system]
         if (input.system) return [input.system]
         if (input.agent.prompt) return [input.agent.prompt]
         if (input.agent.prompt) return [input.agent.prompt]
-        return SystemPrompt.provider(input.modelID)
+        return SystemPrompt.provider(input.model)
       })(),
       })(),
     )
     )
     system.push(...(await SystemPrompt.environment()))
     system.push(...(await SystemPrompt.environment()))
@@ -666,10 +655,7 @@ export namespace SessionPrompt {
 
 
   async function resolveTools(input: {
   async function resolveTools(input: {
     agent: Agent.Info
     agent: Agent.Info
-    model: {
-      providerID: string
-      modelID: string
-    }
+    model: Provider.Model
     sessionID: string
     sessionID: string
     tools?: Record<string, boolean>
     tools?: Record<string, boolean>
     processor: SessionProcessor.Info
     processor: SessionProcessor.Info
@@ -677,16 +663,12 @@ export namespace SessionPrompt {
     const tools: Record<string, AITool> = {}
     const tools: Record<string, AITool> = {}
     const enabledTools = pipe(
     const enabledTools = pipe(
       input.agent.tools,
       input.agent.tools,
-      mergeDeep(await ToolRegistry.enabled(input.model.providerID, input.model.modelID, input.agent)),
+      mergeDeep(await ToolRegistry.enabled(input.agent)),
       mergeDeep(input.tools ?? {}),
       mergeDeep(input.tools ?? {}),
     )
     )
-    for (const item of await ToolRegistry.tools(input.model.providerID, input.model.modelID)) {
+    for (const item of await ToolRegistry.tools(input.model.providerID)) {
       if (Wildcard.all(item.id, enabledTools) === false) continue
       if (Wildcard.all(item.id, enabledTools) === false) continue
-      const schema = ProviderTransform.schema(
-        input.model.providerID,
-        input.model.modelID,
-        z.toJSONSchema(item.parameters),
-      )
+      const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
       tools[item.id] = tool({
       tools[item.id] = tool({
         id: item.id as any,
         id: item.id as any,
         description: item.description,
         description: item.description,
@@ -1437,25 +1419,18 @@ export namespace SessionPrompt {
     if (!isFirst) return
     if (!isFirst) return
     const small =
     const small =
       (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
       (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
+    const language = await Provider.getLanguage(small)
     const provider = await Provider.getProvider(small.providerID)
     const provider = await Provider.getProvider(small.providerID)
     const options = pipe(
     const options = pipe(
       {},
       {},
-      mergeDeep(
-        ProviderTransform.options(
-          small.providerID,
-          small.modelID,
-          small.npm ?? "",
-          input.session.id,
-          provider?.options,
-        ),
-      ),
-      mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, modelID: small.modelID })),
-      mergeDeep(small.info.options),
+      mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)),
+      mergeDeep(ProviderTransform.smallOptions(small)),
+      mergeDeep(small.options),
     )
     )
     await generateText({
     await generateText({
       // use higher # for reasoning models since reasoning tokens eat up a lot of the budget
       // use higher # for reasoning models since reasoning tokens eat up a lot of the budget
-      maxOutputTokens: small.info.reasoning ? 3000 : 20,
-      providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
+      maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
+      providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
       messages: [
       messages: [
         ...SystemPrompt.title(small.providerID).map(
         ...SystemPrompt.title(small.providerID).map(
           (x): ModelMessage => ({
           (x): ModelMessage => ({
@@ -1486,8 +1461,8 @@ export namespace SessionPrompt {
           },
           },
         ]),
         ]),
       ],
       ],
-      headers: small.info.headers,
-      model: small.language,
+      headers: small.headers,
+      model: language,
     })
     })
       .then((result) => {
       .then((result) => {
         if (result.text)
         if (result.text)
@@ -1504,7 +1479,7 @@ export namespace SessionPrompt {
           })
           })
       })
       })
       .catch((error) => {
       .catch((error) => {
-        log.error("failed to generate title", { error, model: small.info.id })
+        log.error("failed to generate title", { error, model: small.id })
       })
       })
   }
   }
 }
 }

+ 11 - 10
packages/opencode/src/session/summary.ts

@@ -76,19 +76,20 @@ export namespace SessionSummary {
     const small =
     const small =
       (await Provider.getSmallModel(assistantMsg.providerID)) ??
       (await Provider.getSmallModel(assistantMsg.providerID)) ??
       (await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID))
       (await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID))
+    const language = await Provider.getLanguage(small)
 
 
     const options = pipe(
     const options = pipe(
       {},
       {},
-      mergeDeep(ProviderTransform.options(small.providerID, small.modelID, small.npm ?? "", assistantMsg.sessionID)),
-      mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, modelID: small.modelID })),
-      mergeDeep(small.info.options),
+      mergeDeep(ProviderTransform.options(small, assistantMsg.sessionID)),
+      mergeDeep(ProviderTransform.smallOptions(small)),
+      mergeDeep(small.options),
     )
     )
 
 
     const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
     const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
     if (textPart && !userMsg.summary?.title) {
     if (textPart && !userMsg.summary?.title) {
       const result = await generateText({
       const result = await generateText({
-        maxOutputTokens: small.info.reasoning ? 1500 : 20,
-        providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
+        maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
+        providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
         messages: [
         messages: [
           ...SystemPrompt.title(small.providerID).map(
           ...SystemPrompt.title(small.providerID).map(
             (x): ModelMessage => ({
             (x): ModelMessage => ({
@@ -106,8 +107,8 @@ export namespace SessionSummary {
             `,
             `,
           },
           },
         ],
         ],
-        headers: small.info.headers,
-        model: small.language,
+        headers: small.headers,
+        model: language,
       })
       })
       log.info("title", { title: result.text })
       log.info("title", { title: result.text })
       userMsg.summary.title = result.text
       userMsg.summary.title = result.text
@@ -132,9 +133,9 @@ export namespace SessionSummary {
           }
           }
         }
         }
         const result = await generateText({
         const result = await generateText({
-          model: small.language,
+          model: language,
           maxOutputTokens: 100,
           maxOutputTokens: 100,
-          providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
+          providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
           messages: [
           messages: [
             ...SystemPrompt.summarize(small.providerID).map(
             ...SystemPrompt.summarize(small.providerID).map(
               (x): ModelMessage => ({
               (x): ModelMessage => ({
@@ -148,7 +149,7 @@ export namespace SessionSummary {
               content: `Summarize the above conversation according to your system prompts.`,
               content: `Summarize the above conversation according to your system prompts.`,
             },
             },
           ],
           ],
-          headers: small.info.headers,
+          headers: small.headers,
         }).catch(() => {})
         }).catch(() => {})
         if (result) summary = result.text
         if (result) summary = result.text
       }
       }

+ 8 - 6
packages/opencode/src/session/system.ts

@@ -17,6 +17,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
 import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
 import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
 import PROMPT_TITLE from "./prompt/title.txt"
 import PROMPT_TITLE from "./prompt/title.txt"
 import PROMPT_CODEX from "./prompt/codex.txt"
 import PROMPT_CODEX from "./prompt/codex.txt"
+import type { Provider } from "@/provider/provider"
 
 
 export namespace SystemPrompt {
 export namespace SystemPrompt {
   export function header(providerID: string) {
   export function header(providerID: string) {
@@ -24,12 +25,13 @@ export namespace SystemPrompt {
     return []
     return []
   }
   }
 
 
-  export function provider(modelID: string) {
-    if (modelID.includes("gpt-5")) return [PROMPT_CODEX]
-    if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
-    if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
-    if (modelID.includes("claude")) return [PROMPT_ANTHROPIC]
-    if (modelID.includes("polaris-alpha")) return [PROMPT_POLARIS]
+  export function provider(model: Provider.Model) {
+    if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX]
+    if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3"))
+      return [PROMPT_BEAST]
+    if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
+    if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
+    if (model.api.id.includes("polaris-alpha")) return [PROMPT_POLARIS]
     return [PROMPT_ANTHROPIC_WITHOUT_TODO]
     return [PROMPT_ANTHROPIC_WITHOUT_TODO]
   }
   }
 
 

+ 3 - 4
packages/opencode/src/share/share-next.ts

@@ -1,7 +1,6 @@
 import { Bus } from "@/bus"
 import { Bus } from "@/bus"
 import { Config } from "@/config/config"
 import { Config } from "@/config/config"
 import { ulid } from "ulid"
 import { ulid } from "ulid"
-import type { ModelsDev } from "@/provider/models"
 import { Provider } from "@/provider/provider"
 import { Provider } from "@/provider/provider"
 import { Session } from "@/session"
 import { Session } from "@/session"
 import { MessageV2 } from "@/session/message-v2"
 import { MessageV2 } from "@/session/message-v2"
@@ -36,7 +35,7 @@ export namespace ShareNext {
             type: "model",
             type: "model",
             data: [
             data: [
               await Provider.getModel(evt.properties.info.model.providerID, evt.properties.info.model.modelID).then(
               await Provider.getModel(evt.properties.info.model.providerID, evt.properties.info.model.modelID).then(
-                (m) => m.info,
+                (m) => m,
               ),
               ),
             ],
             ],
           },
           },
@@ -105,7 +104,7 @@ export namespace ShareNext {
       }
       }
     | {
     | {
         type: "model"
         type: "model"
-        data: ModelsDev.Model[]
+        data: SDK.Model[]
       }
       }
 
 
   const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
   const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
@@ -171,7 +170,7 @@ export namespace ShareNext {
       messages
       messages
         .filter((m) => m.info.role === "user")
         .filter((m) => m.info.role === "user")
         .map((m) => (m.info as SDK.UserMessage).model)
         .map((m) => (m.info as SDK.UserMessage).model)
-        .map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m.info)),
+        .map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m)),
     )
     )
     await sync(sessionID, [
     await sync(sessionID, [
       {
       {

+ 1 - 1
packages/opencode/src/tool/batch.ts

@@ -37,7 +37,7 @@ export const BatchTool = Tool.define("batch", async () => {
       const discardedCalls = params.tool_calls.slice(10)
       const discardedCalls = params.tool_calls.slice(10)
 
 
       const { ToolRegistry } = await import("./registry")
       const { ToolRegistry } = await import("./registry")
-      const availableTools = await ToolRegistry.tools("", "")
+      const availableTools = await ToolRegistry.tools("")
       const toolMap = new Map(availableTools.map((t) => [t.id, t]))
       const toolMap = new Map(availableTools.map((t) => [t.id, t]))
 
 
       const executeCall = async (call: (typeof toolCalls)[0]) => {
       const executeCall = async (call: (typeof toolCalls)[0]) => {

+ 1 - 1
packages/opencode/src/tool/read.ts

@@ -101,7 +101,7 @@ export const ReadTool = Tool.define("read", {
       const modelID = ctx.extra["modelID"] as string
       const modelID = ctx.extra["modelID"] as string
       const model = await Provider.getModel(providerID, modelID).catch(() => undefined)
       const model = await Provider.getModel(providerID, modelID).catch(() => undefined)
       if (!model) return false
       if (!model) return false
-      return model.info.modalities?.input?.includes("image") ?? false
+      return model.capabilities.input.image
     })()
     })()
     if (isImage) {
     if (isImage) {
       if (!supportsImages) {
       if (!supportsImages) {

+ 2 - 6
packages/opencode/src/tool/registry.ts

@@ -108,7 +108,7 @@ export namespace ToolRegistry {
     return all().then((x) => x.map((t) => t.id))
     return all().then((x) => x.map((t) => t.id))
   }
   }
 
 
-  export async function tools(providerID: string, _modelID: string) {
+  export async function tools(providerID: string) {
     const tools = await all()
     const tools = await all()
     const result = await Promise.all(
     const result = await Promise.all(
       tools
       tools
@@ -124,11 +124,7 @@ export namespace ToolRegistry {
     return result
     return result
   }
   }
 
 
-  export async function enabled(
-    _providerID: string,
-    _modelID: string,
-    agent: Agent.Info,
-  ): Promise<Record<string, boolean>> {
+  export async function enabled(agent: Agent.Info): Promise<Record<string, boolean>> {
     const result: Record<string, boolean> = {}
     const result: Record<string, boolean> = {}
 
 
     if (agent.permission.edit === "deny") {
     if (agent.permission.edit === "deny") {

+ 51 - 51
packages/opencode/test/provider/provider.test.ts

@@ -132,7 +132,7 @@ test("model whitelist filters models for provider", async () => {
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
       expect(providers["anthropic"]).toBeDefined()
       expect(providers["anthropic"]).toBeDefined()
-      const models = Object.keys(providers["anthropic"].info.models)
+      const models = Object.keys(providers["anthropic"].models)
       expect(models).toContain("claude-sonnet-4-20250514")
       expect(models).toContain("claude-sonnet-4-20250514")
       expect(models.length).toBe(1)
       expect(models.length).toBe(1)
     },
     },
@@ -163,7 +163,7 @@ test("model blacklist excludes specific models", async () => {
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
       expect(providers["anthropic"]).toBeDefined()
       expect(providers["anthropic"]).toBeDefined()
-      const models = Object.keys(providers["anthropic"].info.models)
+      const models = Object.keys(providers["anthropic"].models)
       expect(models).not.toContain("claude-sonnet-4-20250514")
       expect(models).not.toContain("claude-sonnet-4-20250514")
     },
     },
   })
   })
@@ -198,8 +198,8 @@ test("custom model alias via config", async () => {
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
       expect(providers["anthropic"]).toBeDefined()
       expect(providers["anthropic"]).toBeDefined()
-      expect(providers["anthropic"].info.models["my-alias"]).toBeDefined()
-      expect(providers["anthropic"].info.models["my-alias"].name).toBe("My Custom Alias")
+      expect(providers["anthropic"].models["my-alias"]).toBeDefined()
+      expect(providers["anthropic"].models["my-alias"].name).toBe("My Custom Alias")
     },
     },
   })
   })
 })
 })
@@ -241,8 +241,8 @@ test("custom provider with npm package", async () => {
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
       expect(providers["custom-provider"]).toBeDefined()
       expect(providers["custom-provider"]).toBeDefined()
-      expect(providers["custom-provider"].info.name).toBe("Custom Provider")
-      expect(providers["custom-provider"].info.models["custom-model"]).toBeDefined()
+      expect(providers["custom-provider"].name).toBe("Custom Provider")
+      expect(providers["custom-provider"].models["custom-model"]).toBeDefined()
     },
     },
   })
   })
 })
 })
@@ -299,8 +299,9 @@ test("getModel returns model for valid provider/model", async () => {
       const model = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
       const model = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
       expect(model).toBeDefined()
       expect(model).toBeDefined()
       expect(model.providerID).toBe("anthropic")
       expect(model.providerID).toBe("anthropic")
-      expect(model.modelID).toBe("claude-sonnet-4-20250514")
-      expect(model.language).toBeDefined()
+      expect(model.id).toBe("claude-sonnet-4-20250514")
+      const language = await Provider.getLanguage(model)
+      expect(language).toBeDefined()
     },
     },
   })
   })
 })
 })
@@ -478,11 +479,11 @@ test("model cost defaults to zero when not specified", async () => {
     directory: tmp.path,
     directory: tmp.path,
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      const model = providers["test-provider"].info.models["test-model"]
+      const model = providers["test-provider"].models["test-model"]
       expect(model.cost.input).toBe(0)
       expect(model.cost.input).toBe(0)
       expect(model.cost.output).toBe(0)
       expect(model.cost.output).toBe(0)
-      expect(model.cost.cache_read).toBe(0)
-      expect(model.cost.cache_write).toBe(0)
+      expect(model.cost.cache.read).toBe(0)
+      expect(model.cost.cache.write).toBe(0)
     },
     },
   })
   })
 })
 })
@@ -516,7 +517,7 @@ test("model options are merged from existing model", async () => {
     },
     },
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"]
+      const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
       expect(model.options.customOption).toBe("custom-value")
       expect(model.options.customOption).toBe("custom-value")
     },
     },
   })
   })
@@ -623,17 +624,17 @@ test("getModel uses realIdByKey for aliased models", async () => {
     },
     },
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      expect(providers["anthropic"].info.models["my-sonnet"]).toBeDefined()
+      expect(providers["anthropic"].models["my-sonnet"]).toBeDefined()
 
 
       const model = await Provider.getModel("anthropic", "my-sonnet")
       const model = await Provider.getModel("anthropic", "my-sonnet")
       expect(model).toBeDefined()
       expect(model).toBeDefined()
-      expect(model.modelID).toBe("my-sonnet")
-      expect(model.info.name).toBe("My Sonnet Alias")
+      expect(model.id).toBe("my-sonnet")
+      expect(model.name).toBe("My Sonnet Alias")
     },
     },
   })
   })
 })
 })
 
 
-test("provider api field sets default baseURL", async () => {
+test("provider api field sets model api.url", async () => {
   await using tmp = await tmpdir({
   await using tmp = await tmpdir({
     init: async (dir) => {
     init: async (dir) => {
       await Bun.write(
       await Bun.write(
@@ -666,7 +667,8 @@ test("provider api field sets default baseURL", async () => {
     directory: tmp.path,
     directory: tmp.path,
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      expect(providers["custom-api"].options.baseURL).toBe("https://api.example.com/v1")
+      // api field is stored on model.api.url, used by getSDK to set baseURL
+      expect(providers["custom-api"].models["model-1"].api.url).toBe("https://api.example.com/v1")
     },
     },
   })
   })
 })
 })
@@ -737,10 +739,10 @@ test("model inherits properties from existing database model", async () => {
     },
     },
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"]
+      const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
       expect(model.name).toBe("Custom Name for Sonnet")
       expect(model.name).toBe("Custom Name for Sonnet")
-      expect(model.tool_call).toBe(true)
-      expect(model.attachment).toBe(true)
+      expect(model.capabilities.toolcall).toBe(true)
+      expect(model.capabilities.attachment).toBe(true)
       expect(model.limit.context).toBeGreaterThan(0)
       expect(model.limit.context).toBeGreaterThan(0)
     },
     },
   })
   })
@@ -820,7 +822,7 @@ test("whitelist and blacklist can be combined", async () => {
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
       expect(providers["anthropic"]).toBeDefined()
       expect(providers["anthropic"]).toBeDefined()
-      const models = Object.keys(providers["anthropic"].info.models)
+      const models = Object.keys(providers["anthropic"].models)
       expect(models).toContain("claude-sonnet-4-20250514")
       expect(models).toContain("claude-sonnet-4-20250514")
       expect(models).not.toContain("claude-opus-4-20250514")
       expect(models).not.toContain("claude-opus-4-20250514")
       expect(models.length).toBe(1)
       expect(models.length).toBe(1)
@@ -858,11 +860,9 @@ test("model modalities default correctly", async () => {
     directory: tmp.path,
     directory: tmp.path,
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      const model = providers["test-provider"].info.models["test-model"]
-      expect(model.modalities).toEqual({
-        input: ["text"],
-        output: ["text"],
-      })
+      const model = providers["test-provider"].models["test-model"]
+      expect(model.capabilities.input.text).toBe(true)
+      expect(model.capabilities.output.text).toBe(true)
     },
     },
   })
   })
 })
 })
@@ -903,11 +903,11 @@ test("model with custom cost values", async () => {
     directory: tmp.path,
     directory: tmp.path,
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      const model = providers["test-provider"].info.models["test-model"]
+      const model = providers["test-provider"].models["test-model"]
       expect(model.cost.input).toBe(5)
       expect(model.cost.input).toBe(5)
       expect(model.cost.output).toBe(15)
       expect(model.cost.output).toBe(15)
-      expect(model.cost.cache_read).toBe(2.5)
-      expect(model.cost.cache_write).toBe(7.5)
+      expect(model.cost.cache.read).toBe(2.5)
+      expect(model.cost.cache.write).toBe(7.5)
     },
     },
   })
   })
 })
 })
@@ -931,7 +931,7 @@ test("getSmallModel returns appropriate small model", async () => {
     fn: async () => {
     fn: async () => {
       const model = await Provider.getSmallModel("anthropic")
       const model = await Provider.getSmallModel("anthropic")
       expect(model).toBeDefined()
       expect(model).toBeDefined()
-      expect(model?.modelID).toContain("haiku")
+      expect(model?.id).toContain("haiku")
     },
     },
   })
   })
 })
 })
@@ -957,7 +957,7 @@ test("getSmallModel respects config small_model override", async () => {
       const model = await Provider.getSmallModel("anthropic")
       const model = await Provider.getSmallModel("anthropic")
       expect(model).toBeDefined()
       expect(model).toBeDefined()
       expect(model?.providerID).toBe("anthropic")
       expect(model?.providerID).toBe("anthropic")
-      expect(model?.modelID).toBe("claude-sonnet-4-20250514")
+      expect(model?.id).toBe("claude-sonnet-4-20250514")
     },
     },
   })
   })
 })
 })
@@ -1046,7 +1046,7 @@ test("provider with custom npm package", async () => {
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
       expect(providers["local-llm"]).toBeDefined()
       expect(providers["local-llm"]).toBeDefined()
-      expect(providers["local-llm"].info.npm).toBe("@ai-sdk/openai-compatible")
+      expect(providers["local-llm"].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible")
       expect(providers["local-llm"].options.baseURL).toBe("http://localhost:11434/v1")
       expect(providers["local-llm"].options.baseURL).toBe("http://localhost:11434/v1")
     },
     },
   })
   })
@@ -1082,7 +1082,7 @@ test("model alias name defaults to alias key when id differs", async () => {
     },
     },
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      expect(providers["anthropic"].info.models["sonnet"].name).toBe("sonnet")
+      expect(providers["anthropic"].models["sonnet"].name).toBe("sonnet")
     },
     },
   })
   })
 })
 })
@@ -1123,8 +1123,8 @@ test("provider with multiple env var options only includes apiKey when single en
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
       expect(providers["multi-env"]).toBeDefined()
       expect(providers["multi-env"]).toBeDefined()
-      // When multiple env options exist, apiKey should NOT be auto-set
-      expect(providers["multi-env"].options.apiKey).toBeUndefined()
+      // When multiple env options exist, key should NOT be auto-set
+      expect(providers["multi-env"].key).toBeUndefined()
     },
     },
   })
   })
 })
 })
@@ -1165,8 +1165,8 @@ test("provider with single env var includes apiKey automatically", async () => {
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
       expect(providers["single-env"]).toBeDefined()
       expect(providers["single-env"]).toBeDefined()
-      // Single env option should auto-set apiKey
-      expect(providers["single-env"].options.apiKey).toBe("my-api-key")
+      // Single env option should auto-set key
+      expect(providers["single-env"].key).toBe("my-api-key")
     },
     },
   })
   })
 })
 })
@@ -1201,7 +1201,7 @@ test("model cost overrides existing cost values", async () => {
     },
     },
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"]
+      const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
       expect(model.cost.input).toBe(999)
       expect(model.cost.input).toBe(999)
       expect(model.cost.output).toBe(888)
       expect(model.cost.output).toBe(888)
     },
     },
@@ -1249,11 +1249,11 @@ test("completely new provider not in database can be configured", async () => {
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
       expect(providers["brand-new-provider"]).toBeDefined()
       expect(providers["brand-new-provider"]).toBeDefined()
-      expect(providers["brand-new-provider"].info.name).toBe("Brand New")
-      const model = providers["brand-new-provider"].info.models["new-model"]
-      expect(model.reasoning).toBe(true)
-      expect(model.attachment).toBe(true)
-      expect(model.modalities?.input).toContain("image")
+      expect(providers["brand-new-provider"].name).toBe("Brand New")
+      const model = providers["brand-new-provider"].models["new-model"]
+      expect(model.capabilities.reasoning).toBe(true)
+      expect(model.capabilities.attachment).toBe(true)
+      expect(model.capabilities.input.image).toBe(true)
     },
     },
   })
   })
 })
 })
@@ -1322,7 +1322,7 @@ test("model with tool_call false", async () => {
     directory: tmp.path,
     directory: tmp.path,
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      expect(providers["no-tools"].info.models["basic-model"].tool_call).toBe(false)
+      expect(providers["no-tools"].models["basic-model"].capabilities.toolcall).toBe(false)
     },
     },
   })
   })
 })
 })
@@ -1357,7 +1357,7 @@ test("model defaults tool_call to true when not specified", async () => {
     directory: tmp.path,
     directory: tmp.path,
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      expect(providers["default-tools"].info.models["model"].tool_call).toBe(true)
+      expect(providers["default-tools"].models["model"].capabilities.toolcall).toBe(true)
     },
     },
   })
   })
 })
 })
@@ -1396,7 +1396,7 @@ test("model headers are preserved", async () => {
     directory: tmp.path,
     directory: tmp.path,
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      const model = providers["headers-provider"].info.models["model"]
+      const model = providers["headers-provider"].models["model"]
       expect(model.headers).toEqual({
       expect(model.headers).toEqual({
         "X-Custom-Header": "custom-value",
         "X-Custom-Header": "custom-value",
         Authorization: "Bearer special-token",
         Authorization: "Bearer special-token",
@@ -1465,8 +1465,8 @@ test("getModel returns consistent results", async () => {
       const model1 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
       const model1 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
       const model2 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
       const model2 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
       expect(model1.providerID).toEqual(model2.providerID)
       expect(model1.providerID).toEqual(model2.providerID)
-      expect(model1.modelID).toEqual(model2.modelID)
-      expect(model1.info).toEqual(model2.info)
+      expect(model1.id).toEqual(model2.id)
+      expect(model1).toEqual(model2)
     },
     },
   })
   })
 })
 })
@@ -1501,7 +1501,7 @@ test("provider name defaults to id when not in database", async () => {
     directory: tmp.path,
     directory: tmp.path,
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      expect(providers["my-custom-id"].info.name).toBe("my-custom-id")
+      expect(providers["my-custom-id"].name).toBe("my-custom-id")
     },
     },
   })
   })
 })
 })
@@ -1601,7 +1601,7 @@ test("getProvider returns provider info", async () => {
     fn: async () => {
     fn: async () => {
       const provider = await Provider.getProvider("anthropic")
       const provider = await Provider.getProvider("anthropic")
       expect(provider).toBeDefined()
       expect(provider).toBeDefined()
-      expect(provider?.info.id).toBe("anthropic")
+      expect(provider?.id).toBe("anthropic")
     },
     },
   })
   })
 })
 })
@@ -1684,7 +1684,7 @@ test("model limit defaults to zero when not specified", async () => {
     directory: tmp.path,
     directory: tmp.path,
     fn: async () => {
     fn: async () => {
       const providers = await Provider.list()
       const providers = await Provider.list()
-      const model = providers["no-limit"].info.models["model"]
+      const model = providers["no-limit"].models["model"]
       expect(model.limit.context).toBe(0)
       expect(model.limit.context).toBe(0)
       expect(model.limit.output).toBe(0)
       expect(model.limit.output).toBe(0)
     },
     },

+ 162 - 92
packages/sdk/js/src/gen/types.gen.ts

@@ -942,6 +942,75 @@ export type AgentConfig = {
     | undefined
     | undefined
 }
 }
 
 
+export type ProviderConfig = {
+  api?: string
+  name?: string
+  env?: Array<string>
+  id?: string
+  npm?: string
+  models?: {
+    [key: string]: {
+      id?: string
+      name?: string
+      release_date?: string
+      attachment?: boolean
+      reasoning?: boolean
+      temperature?: boolean
+      tool_call?: boolean
+      cost?: {
+        input: number
+        output: number
+        cache_read?: number
+        cache_write?: number
+        context_over_200k?: {
+          input: number
+          output: number
+          cache_read?: number
+          cache_write?: number
+        }
+      }
+      limit?: {
+        context: number
+        output: number
+      }
+      modalities?: {
+        input: Array<"text" | "audio" | "image" | "video" | "pdf">
+        output: Array<"text" | "audio" | "image" | "video" | "pdf">
+      }
+      experimental?: boolean
+      status?: "alpha" | "beta" | "deprecated"
+      options?: {
+        [key: string]: unknown
+      }
+      headers?: {
+        [key: string]: string
+      }
+      provider?: {
+        npm: string
+      }
+    }
+  }
+  whitelist?: Array<string>
+  blacklist?: Array<string>
+  options?: {
+    apiKey?: string
+    baseURL?: string
+    /**
+     * GitHub Enterprise URL for copilot authentication
+     */
+    enterpriseUrl?: string
+    /**
+     * Enable promptCacheKey for this provider (default false)
+     */
+    setCacheKey?: boolean
+    /**
+     * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.
+     */
+    timeout?: number | false
+    [key: string]: unknown | string | boolean | (number | false) | undefined
+  }
+}
+
 export type McpLocalConfig = {
 export type McpLocalConfig = {
   /**
   /**
    * Type of MCP server connection
    * Type of MCP server connection
@@ -1100,74 +1169,7 @@ export type Config = {
    * Custom provider configurations and model overrides
    * Custom provider configurations and model overrides
    */
    */
   provider?: {
   provider?: {
-    [key: string]: {
-      api?: string
-      name?: string
-      env?: Array<string>
-      id?: string
-      npm?: string
-      models?: {
-        [key: string]: {
-          id?: string
-          name?: string
-          release_date?: string
-          attachment?: boolean
-          reasoning?: boolean
-          temperature?: boolean
-          tool_call?: boolean
-          cost?: {
-            input: number
-            output: number
-            cache_read?: number
-            cache_write?: number
-            context_over_200k?: {
-              input: number
-              output: number
-              cache_read?: number
-              cache_write?: number
-            }
-          }
-          limit?: {
-            context: number
-            output: number
-          }
-          modalities?: {
-            input: Array<"text" | "audio" | "image" | "video" | "pdf">
-            output: Array<"text" | "audio" | "image" | "video" | "pdf">
-          }
-          experimental?: boolean
-          status?: "alpha" | "beta" | "deprecated"
-          options?: {
-            [key: string]: unknown
-          }
-          headers?: {
-            [key: string]: string
-          }
-          provider?: {
-            npm: string
-          }
-        }
-      }
-      whitelist?: Array<string>
-      blacklist?: Array<string>
-      options?: {
-        apiKey?: string
-        baseURL?: string
-        /**
-         * GitHub Enterprise URL for copilot authentication
-         */
-        enterpriseUrl?: string
-        /**
-         * Enable promptCacheKey for this provider (default false)
-         */
-        setCacheKey?: boolean
-        /**
-         * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.
-         */
-        timeout?: number | false
-        [key: string]: unknown | string | boolean | (number | false) | undefined
-      }
-    }
+    [key: string]: ProviderConfig
   }
   }
   /**
   /**
    * MCP (Model Context Protocol) server configurations
    * MCP (Model Context Protocol) server configurations
@@ -1354,51 +1356,71 @@ export type Command = {
 
 
 export type Model = {
 export type Model = {
   id: string
   id: string
+  providerID: string
+  api: {
+    id: string
+    url: string
+    npm: string
+  }
   name: string
   name: string
-  release_date: string
-  attachment: boolean
-  reasoning: boolean
-  temperature: boolean
-  tool_call: boolean
+  capabilities: {
+    temperature: boolean
+    reasoning: boolean
+    attachment: boolean
+    toolcall: boolean
+    input: {
+      text: boolean
+      audio: boolean
+      image: boolean
+      video: boolean
+      pdf: boolean
+    }
+    output: {
+      text: boolean
+      audio: boolean
+      image: boolean
+      video: boolean
+      pdf: boolean
+    }
+  }
   cost: {
   cost: {
     input: number
     input: number
     output: number
     output: number
-    cache_read?: number
-    cache_write?: number
-    context_over_200k?: {
+    cache: {
+      read: number
+      write: number
+    }
+    experimentalOver200K?: {
       input: number
       input: number
       output: number
       output: number
-      cache_read?: number
-      cache_write?: number
+      cache: {
+        read: number
+        write: number
+      }
     }
     }
   }
   }
   limit: {
   limit: {
     context: number
     context: number
     output: number
     output: number
   }
   }
-  modalities?: {
-    input: Array<"text" | "audio" | "image" | "video" | "pdf">
-    output: Array<"text" | "audio" | "image" | "video" | "pdf">
-  }
-  experimental?: boolean
-  status?: "alpha" | "beta" | "deprecated"
+  status: "alpha" | "beta" | "deprecated" | "active"
   options: {
   options: {
     [key: string]: unknown
     [key: string]: unknown
   }
   }
-  headers?: {
+  headers: {
     [key: string]: string
     [key: string]: string
   }
   }
-  provider?: {
-    npm: string
-  }
 }
 }
 
 
 export type Provider = {
 export type Provider = {
-  api?: string
+  id: string
   name: string
   name: string
+  source: "env" | "config" | "custom" | "api"
   env: Array<string>
   env: Array<string>
-  id: string
-  npm?: string
+  key?: string
+  options: {
+    [key: string]: unknown
+  }
   models: {
   models: {
     [key: string]: Model
     [key: string]: Model
   }
   }
@@ -2665,7 +2687,55 @@ export type ProviderListResponses = {
    * List of providers
    * List of providers
    */
    */
   200: {
   200: {
-    all: Array<Provider>
+    all: Array<{
+      api?: string
+      name: string
+      env: Array<string>
+      id: string
+      npm?: string
+      models: {
+        [key: string]: {
+          id: string
+          name: string
+          release_date: string
+          attachment: boolean
+          reasoning: boolean
+          temperature: boolean
+          tool_call: boolean
+          cost?: {
+            input: number
+            output: number
+            cache_read?: number
+            cache_write?: number
+            context_over_200k?: {
+              input: number
+              output: number
+              cache_read?: number
+              cache_write?: number
+            }
+          }
+          limit: {
+            context: number
+            output: number
+          }
+          modalities?: {
+            input: Array<"text" | "audio" | "image" | "video" | "pdf">
+            output: Array<"text" | "audio" | "image" | "video" | "pdf">
+          }
+          experimental?: boolean
+          status?: "alpha" | "beta" | "deprecated"
+          options: {
+            [key: string]: unknown
+          }
+          headers?: {
+            [key: string]: string
+          }
+          provider?: {
+            npm: string
+          }
+        }
+      }
+    }>
     default: {
     default: {
       [key: string]: string
       [key: string]: string
     }
     }