Dax Raad 4 luni în urmă
părinte
comite
2dbb029472

+ 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" />
                     <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>
-                      <Show when={i.release_date}>
+                      <Show when={false}>
                         <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>
                       </Show>
                     </div>

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

@@ -224,6 +224,7 @@ export namespace Agent {
   export async function generate(input: { description: string }) {
     const defaultModel = await Provider.defaultModel()
     const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
+    const language = await Provider.getLanguage(model)
     const system = SystemPrompt.header(defaultModel.providerID)
     system.push(PROMPT_GENERATE)
     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`,
         },
       ],
-      model: model.language,
+      model: language,
       schema: z.object({
         identifier: 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) {
           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) {
             process.stdout.write(`${providerID}/${modelID}`)
             process.stdout.write(EOL)

+ 44 - 52
packages/opencode/src/provider/models.ts

@@ -9,63 +9,55 @@ export namespace ModelsDev {
   const log = Log.create({ service: "models.dev" })
   const filepath = path.join(Global.Path.cache, "models.json")
 
-  export const Model = z
-    .object({
-      id: z.string(),
-      name: z.string(),
-      target: 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(),
-        output: z.number(),
-        cache_read: z.number().optional(),
-        cache_write: z.number().optional(),
-        context_over_200k: z
-          .object({
-            input: z.number(),
-            output: z.number(),
-            cache_read: z.number().optional(),
-            cache_write: z.number().optional(),
-          })
-          .optional(),
-      }),
-      limit: z.object({
-        context: z.number(),
-        output: z.number(),
-      }),
-      modalities: z
+  export const Model = z.object({
+    id: z.string(),
+    name: z.string(),
+    target: 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(),
+      output: z.number(),
+      cache_read: z.number().optional(),
+      cache_write: z.number().optional(),
+      context_over_200k: z
         .object({
-          input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
-          output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+          input: z.number(),
+          output: z.number(),
+          cache_read: z.number().optional(),
+          cache_write: z.number().optional(),
         })
         .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",
-    })
+    }),
+    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 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>
 

+ 37 - 56
packages/opencode/src/provider/provider.ts

@@ -23,7 +23,7 @@ import { createVertex } from "@ai-sdk/google-vertex"
 import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
 import { createOpenAI } from "@ai-sdk/openai"
 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"
 
 export namespace Provider {
@@ -43,14 +43,13 @@ export namespace Provider {
     "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
   }
 
+  type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
   type CustomLoader = (provider: Info) => Promise<{
     autoload: boolean
-    getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
+    getModel?: CustomModelLoader
     options?: Record<string, any>
   }>
 
-  type Source = "env" | "config" | "custom" | "api"
-
   const CUSTOM_LOADERS: Record<string, CustomLoader> = {
     async anthropic() {
       return {
@@ -314,20 +313,20 @@ export namespace Provider {
         reasoning: z.boolean(),
         attachment: z.boolean(),
         toolcall: z.boolean(),
-        input: {
+        input: z.object({
           text: z.boolean(),
           audio: z.boolean(),
           image: z.boolean(),
           video: z.boolean(),
           pdf: z.boolean(),
-        },
-        output: {
+        }),
+        output: z.object({
           text: z.boolean(),
           audio: z.boolean(),
           image: z.boolean(),
           video: z.boolean(),
           pdf: z.boolean(),
-        },
+        }),
       }),
       cost: z.object({
         input: z.number(),
@@ -433,7 +432,7 @@ export namespace Provider {
     }
   }
 
-  function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
+  export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
     return {
       id: provider.id,
       source: "custom",
@@ -444,11 +443,6 @@ export namespace Provider {
     }
   }
 
-  export type ModelWithStuff = {
-    language: LanguageModel
-    info: Model
-  }
-
   const state = Instance.state(async () => {
     using _ = log.time("state")
     const config = await Config.get()
@@ -464,7 +458,10 @@ export namespace Provider {
     }
 
     const providers: { [providerID: string]: Info } = {}
-    const models = new Map<string, ModelWithStuff>()
+    const languages = new Map<string, LanguageModelV2>()
+    const modelLoaders: {
+      [providerID: string]: CustomModelLoader
+    } = {}
     const sdk = new Map<number, SDK>()
 
     log.info("init")
@@ -631,6 +628,7 @@ export namespace Provider {
       if (disabled.has(providerID)) continue
       const result = await fn(database[providerID])
       if (result && (result.autoload || providers[providerID])) {
+        if (result.getModel) modelLoaders[providerID] = result.getModel
         mergeProvider(providerID, {
           source: "custom",
           options: result.options,
@@ -645,7 +643,6 @@ export namespace Provider {
         env: provider.env,
         name: provider.name,
         options: provider.options,
-        // TODO: merge models
       })
     }
 
@@ -689,9 +686,10 @@ export namespace Provider {
     }
 
     return {
-      models,
+      models: languages,
       providers,
       sdk,
+      modelLoaders,
     }
   })
 
@@ -789,15 +787,7 @@ export namespace Provider {
   }
 
   export async function getModel(providerID: string, modelID: string) {
-    const key = `${providerID}/${modelID}`
     const s = await state()
-    if (s.models.has(key)) return s.models.get(key)!
-
-    log.info("getModel", {
-      providerID,
-      modelID,
-    })
-
     const provider = s.providers[providerID]
     if (!provider) {
       const availableProviders = Object.keys(s.providers)
@@ -813,38 +803,29 @@ export namespace Provider {
       const suggestions = matches.map((m) => m.target)
       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(info)
+    const provider = s.providers[model.providerID]
+    const sdk = await getSDK(model)
 
     try {
-      const language = provider.getModel
-        ? await provider.getModel(sdk, info.api.id, provider.options)
-        : sdk.languageModel(info.api.id)
-      log.info("found", { providerID, modelID })
-      const cached: ModelWithStuff = {
-        info,
-        language,
-      }
-      s.models.set(key, {
-        providerID,
-        modelID,
-        info,
-        language,
-        npm,
-      })
-      return {
-        modelID,
-        providerID,
-        info,
-        language,
-        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) {
       if (e instanceof NoSuchModelError)
         throw new ModelNotFoundError(
           {
-            modelID: modelID,
-            providerID,
+            modelID: model.id,
+            providerID: model.providerID,
           },
           { cause: e },
         )
@@ -857,7 +838,7 @@ export namespace Provider {
     const provider = s.providers[providerID]
     if (!provider) return undefined
     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))
           return {
             providerID,
@@ -893,7 +874,7 @@ export namespace Provider {
         priority = ["gpt-5-nano"]
       }
       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)
         }
       }
@@ -901,7 +882,7 @@ export namespace Provider {
 
     // Check if opencode provider is available before using it
     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")
     }
 
@@ -924,12 +905,12 @@ export namespace Provider {
 
     const provider = await list()
       .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")
-    const [model] = sort(Object.values(provider.info.models))
+    const [model] = sort(Object.values(provider.models))
     if (!model) throw new Error("no models found")
     return {
-      providerID: provider.info.id,
+      providerID: provider.id,
       modelID: model.id,
     }
   }

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

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

+ 8 - 5
packages/opencode/src/server/server.ts

@@ -8,7 +8,7 @@ import { proxy } from "hono/proxy"
 import { Session } from "../session"
 import z from "zod"
 import { Provider } from "../provider/provider"
-import { mapValues } from "remeda"
+import { mapValues, pipe } from "remeda"
 import { NamedError } from "@opencode-ai/util/error"
 import { ModelsDev } from "../provider/models"
 import { Ripgrep } from "../file/ripgrep"
@@ -1025,7 +1025,7 @@ export namespace Server {
         async (c) => {
           c.status(204)
           c.header("Content-Type", "application/json")
-          return stream(c, async (stream) => {
+          return stream(c, async () => {
             const sessionID = c.req.valid("param").id
             const body = c.req.valid("json")
             SessionPrompt.prompt({ ...body, sessionID })
@@ -1231,7 +1231,7 @@ export namespace Server {
                 "application/json": {
                   schema: resolver(
                     z.object({
-                      providers: ModelsDev.Provider.array(),
+                      providers: Provider.Info.array(),
                       default: z.record(z.string(), z.string()),
                     }),
                   ),
@@ -1242,7 +1242,7 @@ export namespace Server {
         }),
         async (c) => {
           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({
             providers: Object.values(providers),
             default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
@@ -1272,7 +1272,10 @@ export namespace Server {
           },
         }),
         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))
           return c.json({
             all: Object.values(providers),

+ 8 - 13
packages/opencode/src/session/compaction.ts

@@ -7,7 +7,6 @@ import { MessageV2 } from "./message-v2"
 import { SystemPrompt } from "./system"
 import { Bus } from "../bus"
 import z from "zod"
-import type { ModelsDev } from "../provider/models"
 import { SessionPrompt } from "./prompt"
 import { Flag } from "../flag/flag"
 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
     const context = input.model.limit.context
     if (context === 0) return false
@@ -98,6 +97,7 @@ export namespace SessionCompaction {
     auto: boolean
   }) {
     const model = await Provider.getModel(input.model.providerID, input.model.modelID)
+    const language = await Provider.getLanguage(model)
     const system = [...SystemPrompt.compaction(model.providerID)]
     const msg = (await Session.updateMessage({
       id: Identifier.ascending("message"),
@@ -126,8 +126,7 @@ export namespace SessionCompaction {
     const processor = SessionProcessor.create({
       assistantMessage: msg,
       sessionID: input.sessionID,
-      providerID: input.model.providerID,
-      model: model.info,
+      model: model,
       abort: input.abort,
     })
     const result = await processor.process({
@@ -139,17 +138,13 @@ export namespace SessionCompaction {
       // set to 0, we handle loop
       maxRetries: 0,
       providerOptions: ProviderTransform.providerOptions(
-        model.npm,
+        model.api.npm,
         model.providerID,
-        pipe(
-          {},
-          mergeDeep(ProviderTransform.options(model.providerID, model.info, model.npm ?? "", input.sessionID)),
-          mergeDeep(model.info.options),
-        ),
+        pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
       ),
-      headers: model.info.headers,
+      headers: model.headers,
       abortSignal: input.abort,
-      tools: model.info.tool_call ? {} : undefined,
+      tools: model.capabilities.toolcall ? {} : undefined,
       messages: [
         ...system.map(
           (x): ModelMessage => ({
@@ -183,7 +178,7 @@ export namespace SessionCompaction {
         },
       ],
       model: wrapLanguageModel({
-        model: model.language,
+        model: language,
         middleware: [
           {
             async transformParams(args) {

+ 6 - 6
packages/opencode/src/session/index.ts

@@ -6,7 +6,6 @@ import { Config } from "../config/config"
 import { Flag } from "../flag/flag"
 import { Identifier } from "../id/id"
 import { Installation } from "../installation"
-import type { ModelsDev } from "../provider/models"
 import { Share } from "../share/share"
 import { Storage } from "../storage/storage"
 import { Log } from "../util/log"
@@ -17,6 +16,7 @@ import { fn } from "@/util/fn"
 import { Command } from "../command"
 import { Snapshot } from "@/snapshot"
 import { ShareNext } from "@/share/share-next"
+import { Provider } from "@/provider/provider"
 
 export namespace Session {
   const log = Log.create({ service: "session" })
@@ -389,7 +389,7 @@ export namespace Session {
 
   export const getUsage = fn(
     z.object({
-      model: z.custom<ModelsDev.Model>(),
+      model: Provider.Model,
       usage: z.custom<LanguageModelUsage>(),
       metadata: z.custom<ProviderMetadata>().optional(),
     }),
@@ -420,16 +420,16 @@ export namespace Session {
       }
 
       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
       return {
         cost: safe(
           new Decimal(0)
             .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.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:
             // charge reasoning tokens at the same rate as output tokens
             .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))

+ 3 - 4
packages/opencode/src/session/processor.ts

@@ -1,4 +1,3 @@
-import type { ModelsDev } from "@/provider/models"
 import { MessageV2 } from "./message-v2"
 import { streamText } from "ai"
 import { Log } from "@/util/log"
@@ -11,6 +10,7 @@ import { SessionSummary } from "./summary"
 import { Bus } from "@/bus"
 import { SessionRetry } from "./retry"
 import { SessionStatus } from "./status"
+import type { Provider } from "@/provider/provider"
 
 export namespace SessionProcessor {
   const DOOM_LOOP_THRESHOLD = 3
@@ -31,8 +31,7 @@ export namespace SessionProcessor {
   export function create(input: {
     assistantMessage: MessageV2.Assistant
     sessionID: string
-    providerID: string
-    model: ModelsDev.Model
+    model: Provider.Model
     abort: AbortSignal
   }) {
     const toolcalls: Record<string, MessageV2.ToolPart> = {}
@@ -341,7 +340,7 @@ export namespace SessionProcessor {
             log.error("process", {
               error: e,
             })
-            const error = MessageV2.fromError(e, { providerID: input.providerID })
+            const error = MessageV2.fromError(e, { providerID: input.sessionID })
             const retry = SessionRetry.retryable(error)
             if (retry !== undefined) {
               attempt++

+ 35 - 45
packages/opencode/src/session/prompt.ts

@@ -47,7 +47,6 @@ import { fn } from "@/util/fn"
 import { SessionProcessor } from "./processor"
 import { TaskTool } from "@/tool/task"
 import { SessionStatus } from "./status"
-import type { ModelsDev } from "@/provider/models"
 
 // @ts-ignore
 globalThis.AI_SDK_LOG_WARNINGS = false
@@ -288,6 +287,7 @@ export namespace SessionPrompt {
         })
 
       const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
+      const language = await Provider.getLanguage(model)
       const task = tasks.pop()
 
       // pending subtask
@@ -311,7 +311,7 @@ export namespace SessionPrompt {
             reasoning: 0,
             cache: { read: 0, write: 0 },
           },
-          modelID: model.modelID,
+          modelID: model.id,
           providerID: model.providerID,
           time: {
             created: Date.now(),
@@ -408,7 +408,7 @@ export namespace SessionPrompt {
           agent: lastUser.agent,
           model: {
             providerID: model.providerID,
-            modelID: model.modelID,
+            modelID: model.id,
           },
           sessionID,
           auto: task.auto,
@@ -421,7 +421,7 @@ export namespace SessionPrompt {
       if (
         lastFinished &&
         lastFinished.summary !== true &&
-        SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model: model.info })
+        SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })
       ) {
         await SessionCompaction.create({
           sessionID,
@@ -455,7 +455,7 @@ export namespace SessionPrompt {
             reasoning: 0,
             cache: { read: 0, write: 0 },
           },
-          modelID: model.modelID,
+          modelID: model.id,
           providerID: model.providerID,
           time: {
             created: Date.now(),
@@ -463,21 +463,18 @@ export namespace SessionPrompt {
           sessionID,
         })) as MessageV2.Assistant,
         sessionID: sessionID,
-        model: model.info,
-        providerID: model.providerID,
+        model,
         abort,
       })
       const system = await resolveSystemPrompt({
-        providerID: model.providerID,
-        model: model.info,
+        model,
         agent,
         system: lastUser.system,
       })
       const tools = await resolveTools({
         agent,
         sessionID,
-        providerID: model.providerID,
-        model: model.info,
+        model,
         tools: lastUser.tools,
         processor,
       })
@@ -487,19 +484,19 @@ export namespace SessionPrompt {
         {
           sessionID: sessionID,
           agent: lastUser.agent,
-          model: model.info,
+          model: model,
           provider,
           message: lastUser,
         },
         {
-          temperature: model.info.temperature
-            ? (agent.temperature ?? ProviderTransform.temperature(model.info))
+          temperature: model.capabilities.temperature
+            ? (agent.temperature ?? ProviderTransform.temperature(model))
             : undefined,
-          topP: agent.topP ?? ProviderTransform.topP(model.info),
+          topP: agent.topP ?? ProviderTransform.topP(model),
           options: pipe(
             {},
-            mergeDeep(ProviderTransform.options(model.providerID, model.info, model.npm, sessionID, provider?.options)),
-            mergeDeep(model.info.options),
+            mergeDeep(ProviderTransform.options(model, sessionID, provider?.options)),
+            mergeDeep(model.options),
             mergeDeep(agent.options),
           ),
         },
@@ -547,19 +544,19 @@ export namespace SessionPrompt {
                 "x-opencode-request": lastUser.id,
               }
             : undefined),
-          ...model.info.headers,
+          ...model.headers,
         },
         // set to 0, we handle loop
         maxRetries: 0,
         activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
         maxOutputTokens: ProviderTransform.maxOutputTokens(
-          model.providerID,
+          model.api.npm,
           params.options,
-          model.info.limit.output,
+          model.limit.output,
           OUTPUT_TOKEN_MAX,
         ),
         abortSignal: abort,
-        providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
+        providerOptions: ProviderTransform.providerOptions(model.api.npm, model.providerID, params.options),
         stopWhen: stepCountIs(1),
         temperature: params.temperature,
         topP: params.topP,
@@ -586,9 +583,9 @@ export namespace SessionPrompt {
             }),
           ),
         ],
-        tools: model.info.tool_call === false ? undefined : tools,
+        tools: model.capabilities.toolcall === false ? undefined : tools,
         model: wrapLanguageModel({
-          model: model.language,
+          model: language,
           middleware: [
             {
               async transformParams(args) {
@@ -604,7 +601,7 @@ export namespace SessionPrompt {
                       // Transform the inputSchema for provider compatibility
                       return {
                         ...tool,
-                        inputSchema: ProviderTransform.schema(model.providerID, model.info, tool.inputSchema),
+                        inputSchema: ProviderTransform.schema(model, tool.inputSchema),
                       }
                     }
                     // If no inputSchema, return tool unchanged
@@ -639,13 +636,8 @@ export namespace SessionPrompt {
     return Provider.defaultModel()
   }
 
-  async function resolveSystemPrompt(input: {
-    system?: string
-    agent: Agent.Info
-    providerID: string
-    model: ModelsDev.Model
-  }) {
-    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(
       ...(() => {
         if (input.system) return [input.system]
@@ -663,8 +655,7 @@ export namespace SessionPrompt {
 
   async function resolveTools(input: {
     agent: Agent.Info
-    providerID: string
-    model: ModelsDev.Model
+    model: Provider.Model
     sessionID: string
     tools?: Record<string, boolean>
     processor: SessionProcessor.Info
@@ -675,9 +666,9 @@ export namespace SessionPrompt {
       mergeDeep(await ToolRegistry.enabled(input.agent)),
       mergeDeep(input.tools ?? {}),
     )
-    for (const item of await ToolRegistry.tools(input.providerID)) {
+    for (const item of await ToolRegistry.tools(input.model.providerID)) {
       if (Wildcard.all(item.id, enabledTools) === false) continue
-      const schema = ProviderTransform.schema(input.providerID, input.model, z.toJSONSchema(item.parameters))
+      const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
       tools[item.id] = tool({
         id: item.id as any,
         description: item.description,
@@ -1428,19 +1419,18 @@ export namespace SessionPrompt {
     if (!isFirst) return
     const small =
       (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 options = pipe(
       {},
-      mergeDeep(
-        ProviderTransform.options(small.providerID, small.info, small.npm ?? "", input.session.id, provider?.options),
-      ),
-      mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, model: small.info })),
-      mergeDeep(small.info.options),
+      mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)),
+      mergeDeep(ProviderTransform.smallOptions(small)),
+      mergeDeep(small.options),
     )
     await generateText({
       // 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: [
         ...SystemPrompt.title(small.providerID).map(
           (x): ModelMessage => ({
@@ -1471,8 +1461,8 @@ export namespace SessionPrompt {
           },
         ]),
       ],
-      headers: small.info.headers,
-      model: small.language,
+      headers: small.headers,
+      model: language,
     })
       .then((result) => {
         if (result.text)
@@ -1489,7 +1479,7 @@ export namespace SessionPrompt {
           })
       })
       .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 =
       (await Provider.getSmallModel(assistantMsg.providerID)) ??
       (await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID))
+    const language = await Provider.getLanguage(small)
 
     const options = pipe(
       {},
-      mergeDeep(ProviderTransform.options(small.providerID, small.info, small.npm ?? "", assistantMsg.sessionID)),
-      mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, model: small.info })),
-      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
     if (textPart && !userMsg.summary?.title) {
       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: [
           ...SystemPrompt.title(small.providerID).map(
             (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 })
       userMsg.summary.title = result.text
@@ -132,9 +133,9 @@ export namespace SessionSummary {
           }
         }
         const result = await generateText({
-          model: small.language,
+          model: language,
           maxOutputTokens: 100,
-          providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
+          providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
           messages: [
             ...SystemPrompt.summarize(small.providerID).map(
               (x): ModelMessage => ({
@@ -148,7 +149,7 @@ export namespace SessionSummary {
               content: `Summarize the above conversation according to your system prompts.`,
             },
           ],
-          headers: small.info.headers,
+          headers: small.headers,
         }).catch(() => {})
         if (result) summary = result.text
       }

+ 7 - 7
packages/opencode/src/session/system.ts

@@ -17,7 +17,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
 import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
 import PROMPT_TITLE from "./prompt/title.txt"
 import PROMPT_CODEX from "./prompt/codex.txt"
-import type { ModelsDev } from "@/provider/models"
+import type { Provider } from "@/provider/provider"
 
 export namespace SystemPrompt {
   export function header(providerID: string) {
@@ -25,13 +25,13 @@ export namespace SystemPrompt {
     return []
   }
 
-  export function provider(model: ModelsDev.Model) {
-    if (model.target.includes("gpt-5")) return [PROMPT_CODEX]
-    if (model.target.includes("gpt-") || model.target.includes("o1") || model.target.includes("o3"))
+  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.target.includes("gemini-")) return [PROMPT_GEMINI]
-    if (model.target.includes("claude")) return [PROMPT_ANTHROPIC]
-    if (model.target.includes("polaris-alpha")) return [PROMPT_POLARIS]
+    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]
   }
 

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

@@ -1,7 +1,6 @@
 import { Bus } from "@/bus"
 import { Config } from "@/config/config"
 import { ulid } from "ulid"
-import type { ModelsDev } from "@/provider/models"
 import { Provider } from "@/provider/provider"
 import { Session } from "@/session"
 import { MessageV2 } from "@/session/message-v2"
@@ -36,7 +35,7 @@ export namespace ShareNext {
             type: "model",
             data: [
               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"
-        data: ModelsDev.Model[]
+        data: SDK.Model[]
       }
 
   const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
@@ -171,7 +170,7 @@ export namespace ShareNext {
       messages
         .filter((m) => m.info.role === "user")
         .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, [
       {

+ 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 model = await Provider.getModel(providerID, modelID).catch(() => undefined)
       if (!model) return false
-      return model.info.modalities?.input?.includes("image") ?? false
+      return model.capabilities.input.image
     })()
     if (isImage) {
       if (!supportsImages) {

+ 164 - 94
packages/sdk/js/src/gen/types.gen.ts

@@ -942,6 +942,76 @@ export type AgentConfig = {
     | undefined
 }
 
+export type ProviderConfig = {
+  api?: string
+  name?: string
+  env?: Array<string>
+  id?: string
+  npm?: string
+  models?: {
+    [key: string]: {
+      id?: string
+      name?: string
+      target?: 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 = {
   /**
    * Type of MCP server connection
@@ -1100,75 +1170,7 @@ export type Config = {
    * Custom provider configurations and model overrides
    */
   provider?: {
-    [key: string]: {
-      api?: string
-      name?: string
-      env?: Array<string>
-      id?: string
-      npm?: string
-      models?: {
-        [key: string]: {
-          id?: string
-          name?: string
-          target?: 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
@@ -1355,52 +1357,71 @@ export type Command = {
 
 export type Model = {
   id: string
+  providerID: string
+  api: {
+    id: string
+    url: string
+    npm: string
+  }
   name: string
-  target: 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: {
     input: number
     output: number
-    cache_read?: number
-    cache_write?: number
-    context_over_200k?: {
+    cache: {
+      read: number
+      write: number
+    }
+    experimentalOver200K?: {
       input: number
       output: number
-      cache_read?: number
-      cache_write?: number
+      cache: {
+        read: number
+        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"
+  status: "alpha" | "beta" | "deprecated" | "active"
   options: {
     [key: string]: unknown
   }
-  headers?: {
+  headers: {
     [key: string]: string
   }
-  provider?: {
-    npm: string
-  }
 }
 
 export type Provider = {
-  api?: string
+  id: string
   name: string
+  source: "env" | "config" | "custom" | "api"
   env: Array<string>
-  id: string
-  npm?: string
+  key?: string
+  options: {
+    [key: string]: unknown
+  }
   models: {
     [key: string]: Model
   }
@@ -2667,7 +2688,56 @@ export type ProviderListResponses = {
    * List of providers
    */
   200: {
-    all: Array<Provider>
+    all: Array<{
+      api?: string
+      name: string
+      env: Array<string>
+      id: string
+      npm?: string
+      models: {
+        [key: string]: {
+          id: string
+          name: string
+          target: 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: {
       [key: string]: string
     }