Kaynağa Gözat

feat(llm): provider packages own model request construction

Provider packages in @opencode-ai/llm/providers/* now implement a uniform
ProviderPackage contract: model(modelID, settings) => Model. SessionRunnerModel
becomes provider-agnostic: it resolves a package specifier, folds catalog
settings, credentials, and transport overlays into one Settings object, and
delegates request construction to the package.

- llm: add ProviderPackage (Settings, Definition, define), flat model(id,
  config) constructors on the openai-responses, anthropic-messages, and
  openai-compatible-chat protocols, contract-shaped model exports on the
  openai, anthropic, and openai-compatible providers, and a new
  providers/openai/codex entry point that targets the ChatGPT codex backend
  and sets the chatgpt-account-id header from settings.accountID.
- schema: Provider.Native gains optional package.
- core: SessionRunnerModel loads packages through a static built-in map
  (dynamic import for foreign specifiers) and applies one settings fold;
  the ChatGPT conditional is deleted from the runner. The OpenAI plugin's
  catalog transform now assigns the codex package to eligible models when a
  ChatGPT connection is active, alongside the existing eligibility and cost
  rewrites.
- llm schema: hoist ToolResultValue union out of its Object.assign self
  reference; the previous shape only typechecked under lucky check ordering
  and broke under core's typecheck with the new import graph.

Closes #34765
Kit Langton 1 ay önce
ebeveyn
işleme
38398fd450

+ 13 - 2
packages/client/src/generated/types.ts

@@ -1955,6 +1955,7 @@ export type ModelsListOutput = {
       | {
           readonly id: string
           readonly type: "native"
+          readonly package?: string
           readonly url?: string
           readonly settings: { readonly [x: string]: JsonValue }
         }
@@ -2010,7 +2011,12 @@ export type ProvidersListOutput = {
           readonly url?: string
           readonly settings?: { readonly [x: string]: JsonValue }
         }
-      | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
+      | {
+          readonly type: "native"
+          readonly package?: string
+          readonly url?: string
+          readonly settings: { readonly [x: string]: JsonValue }
+        }
     readonly request: {
       readonly headers: { readonly [x: string]: string }
       readonly body: { readonly [x: string]: JsonValue }
@@ -2043,7 +2049,12 @@ export type ProvidersGetOutput = {
           readonly url?: string
           readonly settings?: { readonly [x: string]: JsonValue }
         }
-      | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
+      | {
+          readonly type: "native"
+          readonly package?: string
+          readonly url?: string
+          readonly settings: { readonly [x: string]: JsonValue }
+        }
     readonly request: {
       readonly headers: { readonly [x: string]: string }
       readonly body: { readonly [x: string]: JsonValue }

+ 54 - 10
packages/core/src/plugin/provider/openai.ts

@@ -1,9 +1,10 @@
 import { createServer } from "node:http"
 import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
 import { define } from "@opencode-ai/plugin/v2/effect/plugin"
-import { Deferred, Effect } from "effect"
+import { Deferred, Effect, Stream } from "effect"
 import type { Scope } from "effect"
 import { Credential } from "../../credential"
+import { EventV2 } from "../../event"
 import { InstallationVersion } from "../../installation/version"
 import { Integration } from "../../integration"
 import { ModelV2 } from "../../model"
@@ -17,6 +18,7 @@ const callbackPort = 1455
 const pollingSafetyMargin = 3000
 const browserMethodID = Integration.MethodID.make("chatgpt-browser")
 const headlessMethodID = Integration.MethodID.make("chatgpt-headless")
+const codexPackage = "@opencode-ai/llm/providers/openai/codex"
 
 type Pkce = {
   verifier: string
@@ -154,24 +156,55 @@ const headless = {
 export const OpenAIPlugin = define({
   id: "openai",
   effect: Effect.fn(function* (ctx) {
+    const events = yield* EventV2.Service
     yield* ctx.integration.transform((draft) => {
       draft.method.update(browser)
       draft.method.update(headless)
     })
     yield* ctx.catalog.transform(
       Effect.fn(function* (evt) {
+        const connection = yield* ctx.integration.connection.active("openai")
+        const credential = connection
+          ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined)))
+          : undefined
+        const chatgpt = isChatGPT(credential)
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/openai") continue
-          if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
-          evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
-            // OpenAIPlugin sends OpenAI models through Responses; this alias is a
-            // chat-completions-only model, so hide it only from OpenAI's catalog.
-            model.enabled = false
-          })
+          if (item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) {
+            evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
+              // OpenAIPlugin sends OpenAI models through Responses; this alias is a
+              // chat-completions-only model, so hide it only from OpenAI's catalog.
+              model.enabled = false
+            })
+          }
+          if (!chatgpt) continue
+          for (const model of item.models.values()) {
+            evt.model.update(item.provider.id, model.id, (draft) => {
+              if (!eligible(draft)) {
+                draft.enabled = false
+                return
+              }
+              draft.cost = draft.cost.map((cost) => ({ ...cost, input: 0, output: 0, cache: { read: 0, write: 0 } }))
+              draft.api = {
+                type: "native",
+                id: draft.api.id,
+                package: codexPackage,
+                settings:
+                  draft.api.type === "aisdk"
+                    ? { ...item.provider.api.settings, ...draft.api.settings }
+                    : draft.api.settings,
+              }
+            })
+          }
         }
       }),
     )
+    yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
+      Stream.filter((event) => event.data.integrationID === Integration.ID.make("openai")),
+      Stream.runForEach(() => ctx.catalog.reload()),
+      Effect.forkScoped({ startImmediately: true }),
+    )
     yield* ctx.aisdk.sdk(
       Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/openai") return
@@ -269,16 +302,27 @@ function authorizeURL(redirect: string, pkce: Pkce, state: string) {
     codex_cli_simplified_flow: "true",
     state,
     originator: "opencode",
-  })}`
+  }).toString()}`
 }
 
 function extractAccountID(tokens: TokenResponse) {
   return claim(tokens.id_token) ?? claim(tokens.access_token)
 }
 
+function isChatGPT(credential: { readonly type: string; readonly methodID?: string } | undefined) {
+  return (
+    credential?.type === "oauth" &&
+    (credential.methodID === browserMethodID || credential.methodID === headlessMethodID)
+  )
+}
+
+function eligible(model: { readonly id: string }) {
+  return model.id.includes("codex")
+}
+
 function claim(token: string) {
   const part = token.split(".")[1]
-  if (!part) return
+  if (!part) return undefined
   try {
     const claims = JSON.parse(Buffer.from(part, "base64url").toString()) as Claims
     return (
@@ -287,6 +331,6 @@ function claim(token: string) {
       claims.organizations?.[0]?.id
     )
   } catch {
-    return
+    return undefined
   }
 }

+ 44 - 1
packages/core/src/provider.ts

@@ -1,7 +1,9 @@
 export * as ProviderV2 from "./provider"
 
-import { Types } from "effect"
+import { Effect, Schema, Types } from "effect"
 import { Provider } from "@opencode-ai/schema/provider"
+import { ProviderPackage } from "@opencode-ai/llm/provider-package"
+import { Anthropic, OpenAI, OpenAICodex, OpenAICompatible } from "@opencode-ai/llm/providers"
 
 export const ID = Provider.ID
 export type ID = typeof ID.Type
@@ -23,3 +25,44 @@ export const Info = Provider.Info
 export type Info = Provider.Info
 
 export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
+
+export class PackageLoadError extends Schema.TaggedErrorClass<PackageLoadError>()("ProviderV2.PackageLoadError", {
+  specifier: Schema.String,
+  reason: Schema.String,
+}) {
+  override get message() {
+    return `Failed to load provider package ${this.specifier}: ${this.reason}`
+  }
+}
+
+type PackageModule = { readonly model: ProviderPackage.Definition["model"] }
+
+const builtins: Record<string, PackageModule> = {
+  "@opencode-ai/llm/providers/openai": OpenAI,
+  "@opencode-ai/llm/providers/anthropic": Anthropic,
+  "@opencode-ai/llm/providers/openai-compatible": OpenAICompatible,
+  "@opencode-ai/llm/providers/openai/codex": OpenAICodex,
+}
+
+export const loadPackage = (
+  specifier: string,
+): Effect.Effect<ProviderPackage.Definition["model"], PackageLoadError> => {
+  const builtin = builtins[specifier]
+  if (builtin) return Effect.succeed(builtin.model)
+  return Effect.tryPromise({
+    try: () => import(specifier),
+    catch: (cause) =>
+      new PackageLoadError({ specifier, reason: cause instanceof Error ? cause.message : String(cause) }),
+  }).pipe(
+    Effect.flatMap((module) => {
+      if (hasModel(module)) return Effect.succeed(module.model)
+      return Effect.fail(new PackageLoadError({ specifier, reason: "missing model export" }))
+    }),
+  )
+}
+
+export const load = loadPackage
+
+function hasModel(module: unknown): module is PackageModule {
+  return typeof module === "object" && module !== null && "model" in module && typeof module.model === "function"
+}

+ 81 - 58
packages/core/src/session/runner/model.ts

@@ -1,11 +1,7 @@
 export * as SessionRunnerModel from "./model"
 
 import { makeLocationNode } from "../../effect/app-node"
-import { type Model } from "@opencode-ai/llm"
-import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
-import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
-import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses"
-import { Auth, type AnyRoute } from "@opencode-ai/llm/route"
+import { Model } from "@opencode-ai/llm"
 import { Context, Effect, Layer, Schema } from "effect"
 import { produce } from "immer"
 import { Catalog } from "../../catalog"
@@ -69,6 +65,7 @@ export type Error =
   | ModelUnavailableError
   | VariantUnavailableError
   | UnsupportedApiError
+  | ProviderV2.PackageLoadError
   | Integration.AuthorizationError
 
 export interface Interface {
@@ -80,27 +77,6 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
 /** Test or embedding seam for supplying a model resolver directly. */
 export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
 
-const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
-  if (credential?.type === "key") return Auth.value(credential.key)
-  if (credential?.type === "oauth") return Auth.value(credential.access)
-  const value = model.request.body.apiKey ?? model.api.settings?.apiKey
-  if (typeof value === "string") return Auth.value(value)
-}
-
-const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
-  const body = model.request.body
-  const httpBody = Object.hasOwn(body, "apiKey")
-    ? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
-    : body
-  return route.with({
-    provider: model.providerID,
-    endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
-    headers: model.request.headers,
-    http: { body: httpBody },
-    limits: { context: model.limit.context, output: model.limit.output },
-  })
-}
-
 const withVariant = (
   model: ModelV2.Info,
   variantID: ModelV2.VariantID | undefined,
@@ -131,52 +107,99 @@ const apiName = (model: ModelV2.Info) =>
 export const fromCatalogModel = (
   model: ModelV2.Info,
   credential?: Credential.Value,
-): Effect.Effect<Model, UnsupportedApiError> => {
+): Effect.Effect<Model, UnsupportedApiError | ProviderV2.PackageLoadError> => {
   const resolved =
     credential?.type !== "key" || credential.metadata === undefined
       ? model
       : produce(model, (draft) => {
           Object.assign(draft.request.body, credential.metadata)
         })
-  const key = apiKey(resolved, credential)
-  if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
-    return Effect.succeed(
-      withDefaults(resolved, OpenAIResponses.route)
-        .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
-        .model({ id: resolved.api.id }),
-    )
-  }
-  if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/anthropic") {
-    return Effect.succeed(
-      withDefaults(resolved, AnthropicMessages.route)
-        .with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
-        .model({ id: resolved.api.id }),
-    )
-  }
-  if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai-compatible" && resolved.api.url) {
-    return Effect.succeed(
-      withDefaults(resolved, OpenAICompatibleChat.route)
-        .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
-        .model({ id: resolved.api.id }),
-    )
+  return packageSpecifier(resolved).pipe(
+    Effect.flatMap((specifier) =>
+      ProviderV2.loadPackage(specifier).pipe(
+        Effect.map((load) => {
+          const selected = load(resolved.api.id ?? resolved.id, {
+            ...resolved.api.settings,
+            ...(credential?.type === "oauth" ? credential.metadata : undefined),
+            baseURL: resolved.api.url ?? settingsString(resolved.api.settings, "baseURL"),
+            apiKey: apiKey(resolved, credential),
+            providerOptions: requestSettings(resolved.request),
+            headers: resolved.request.headers,
+            body: stripApiKey(resolved.request.body),
+            limits: { context: resolved.limit.context, output: resolved.limit.output },
+          })
+          return Model.update(selected, {
+            provider: resolved.providerID,
+            route: selected.route.with({ provider: resolved.providerID }),
+          })
+        }),
+      ),
+    ),
+  )
+}
+
+export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, credential?: Credential.Value) =>
+  withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
+
+/** Legacy aisdk catalog entries dispatch to the equivalent native provider packages. */
+const aisdkPackages: Record<string, string | undefined> = {
+  "@ai-sdk/openai": "@opencode-ai/llm/providers/openai",
+  "@ai-sdk/anthropic": "@opencode-ai/llm/providers/anthropic",
+  "@ai-sdk/openai-compatible": "@opencode-ai/llm/providers/openai-compatible",
+}
+
+export const supported = (model: ModelV2.Info) =>
+  (model.api.type === "native" && model.api.package !== undefined) ||
+  (model.api.type === "aisdk" &&
+    aisdkPackages[model.api.package] !== undefined &&
+    // The openai-compatible package has no default endpoint; a URL is required.
+    (model.api.package !== "@ai-sdk/openai-compatible" || model.api.url !== undefined))
+
+const packageSpecifier = (model: ModelV2.Info): Effect.Effect<string, UnsupportedApiError> => {
+  if (supported(model)) {
+    if (model.api.type === "native" && model.api.package !== undefined) return Effect.succeed(model.api.package)
+    const specifier = model.api.type === "aisdk" ? aisdkPackages[model.api.package] : undefined
+    if (specifier !== undefined) return Effect.succeed(specifier)
   }
   return Effect.fail(
     new UnsupportedApiError({
-      providerID: resolved.providerID,
-      modelID: resolved.id,
-      api: apiName(resolved),
+      providerID: model.providerID,
+      modelID: model.id,
+      api: apiName(model),
     }),
   )
 }
 
-export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, credential?: Credential.Value) =>
-  withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
+const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
+  if (credential?.type === "key") return credential.key
+  if (credential?.type === "oauth") return credential.access
+  const value = model.request.body.apiKey ?? model.api.settings?.apiKey
+  if (typeof value === "string") return value
+  return undefined
+}
 
-export const supported = (model: ModelV2.Info) =>
-  model.api.type === "aisdk" &&
-  (model.api.package === "@ai-sdk/openai" ||
-    model.api.package === "@ai-sdk/anthropic" ||
-    (model.api.package === "@ai-sdk/openai-compatible" && model.api.url !== undefined))
+const stripApiKey = (body: ModelV2.Info["request"]["body"]) => {
+  if (!Object.hasOwn(body, "apiKey")) return body
+  return Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
+}
+
+const requestSettings = (request: ModelV2.Info["request"]) => {
+  if (!("settings" in request)) return undefined
+  const settings = request.settings
+  if (!isRecord(settings)) return undefined
+  if (Object.keys(settings).length === 0) return undefined
+  return settings
+}
+
+const settingsString = (settings: ModelV2.Info["api"]["settings"], key: string) => {
+  const value = settings?.[key]
+  if (typeof value === "string") return value
+  return undefined
+}
+
+function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
+  return typeof value === "object" && value !== null && !Array.isArray(value)
+}
 
 /** Resolves models from the catalog belonging to the current Location runtime. */
 export const locationLayer = Layer.effect(

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

@@ -3,6 +3,7 @@ import { describe, expect } from "bun:test"
 import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
+import { Credential } from "@opencode-ai/core/credential"
 import { Integration } from "@opencode-ai/core/integration"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
@@ -16,7 +17,6 @@ const it = testEffect(PluginTestLayer)
 
 const addPlugin = Effect.fn(function* () {
   const plugin = yield* PluginV2.Service
-  const aisdk = yield* AISDK.Service
   const host = yield* PluginHost.make(plugin)
   const integrations = yield* Integration.Service
   yield* OpenAIPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integrations))
@@ -61,7 +61,6 @@ describe("OpenAIPlugin", () => {
 
   it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
       yield* addPlugin()
       const result = yield* aisdk.runSDK({
@@ -78,7 +77,6 @@ describe("OpenAIPlugin", () => {
 
   it.effect("ignores non-OpenAI SDK packages", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
       yield* addPlugin()
       const result = yield* aisdk.runSDK({
@@ -95,7 +93,6 @@ describe("OpenAIPlugin", () => {
 
   it.effect("uses the Responses API for language models", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
       const calls: string[] = []
       yield* addPlugin()
@@ -114,7 +111,6 @@ describe("OpenAIPlugin", () => {
 
   it.effect("ignores non-OpenAI providers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
       const calls: string[] = []
       yield* addPlugin()
@@ -173,4 +169,52 @@ describe("OpenAIPlugin", () => {
       ).toBe(true)
     }),
   )
+
+  it.effect("routes eligible models to the codex package for ChatGPT connections", () =>
+    Effect.gen(function* () {
+      const catalog = yield* Catalog.Service
+      const credentials = yield* Credential.Service
+      yield* catalog.transform((catalog) => {
+        const item = ProviderV2.Info.make({
+          ...ProviderV2.Info.empty(ProviderV2.ID.openai),
+          api: { type: "aisdk", package: "@ai-sdk/openai", settings: { store: false } },
+        })
+        catalog.provider.update(item.id, (draft) => {
+          draft.api = item.api
+        })
+        catalog.model.update(item.id, ModelV2.ID.make("gpt-5-codex"), (draft) => {
+          draft.api = { id: ModelV2.ID.make("gpt-5-codex"), type: "aisdk", package: "@ai-sdk/openai" }
+          draft.cost = [{ input: 1, output: 2, cache: { read: 3, write: 4 } }]
+        })
+        catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), (draft) => {
+          draft.api = { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "@ai-sdk/openai" }
+        })
+      })
+      yield* credentials.create({
+        integrationID: Integration.ID.make("openai"),
+        value: Credential.OAuth.make({
+          type: "oauth",
+          methodID: Integration.MethodID.make("chatgpt-browser"),
+          access: "access",
+          refresh: "refresh",
+          expires: Date.now() + 60_000,
+          metadata: { accountID: "account-123" },
+        }),
+      })
+
+      yield* addPlugin()
+
+      expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-codex")))).toMatchObject({
+        enabled: true,
+        api: {
+          type: "native",
+          id: "gpt-5-codex",
+          package: "@opencode-ai/llm/providers/openai/codex",
+          settings: { store: false },
+        },
+        cost: [{ input: 0, output: 0, cache: { read: 0, write: 0 } }],
+      })
+      expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(false)
+    }),
+  )
 })

+ 76 - 1
packages/core/test/session-runner-model.test.ts

@@ -20,7 +20,12 @@ type Api =
       readonly url?: string
       readonly settings?: Record<string, unknown>
     }
-  | { readonly type: "native"; readonly url?: string; readonly settings: Record<string, unknown> }
+  | {
+      readonly type: "native"
+      readonly package?: string
+      readonly url?: string
+      readonly settings: Record<string, unknown>
+    }
 
 const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
   ModelV2.Info.make({
@@ -269,6 +274,71 @@ describe("SessionRunnerModel", () => {
     }),
   )
 
+  it.effect("maps native provider package models into bearer-authenticated routes", () =>
+    Effect.gen(function* () {
+      const resolved = yield* SessionRunnerModel.fromCatalogModel(
+        ModelV2.Info.make({
+          ...model({
+            type: "native",
+            package: "@opencode-ai/llm/providers/openai",
+            url: "https://openai.example/v1",
+            settings: {},
+          }),
+          request: { headers: {}, body: {} },
+        }),
+        Credential.Key.make({ type: "key", key: "secret" }),
+      )
+      const headers = yield* resolved.route.auth.apply({
+        request: LLM.request({ model: resolved, prompt: "Hello" }),
+        method: "POST",
+        url: "https://openai.example/v1/responses",
+        body: "{}",
+        headers: Headers.empty,
+      })
+
+      expect(resolved.route).toMatchObject({
+        id: "openai-responses",
+        endpoint: { baseURL: "https://openai.example/v1" },
+      })
+      expect(headers.authorization).toBe("Bearer secret")
+    }),
+  )
+
+  it.effect("routes native ChatGPT OAuth credentials to the codex backend", () =>
+    Effect.gen(function* () {
+      const resolved = yield* SessionRunnerModel.fromCatalogModel(
+        ModelV2.Info.make({
+          ...model({
+            type: "native",
+            package: "@opencode-ai/llm/providers/openai/codex",
+            settings: {},
+          }),
+          request: { headers: {}, body: {} },
+        }),
+        Credential.OAuth.make({
+          type: "oauth",
+          methodID: Integration.MethodID.make("chatgpt-browser"),
+          access: "oauth-token",
+          refresh: "refresh",
+          expires: Date.now() + 60_000,
+          metadata: { accountID: "account-123" },
+        }),
+      )
+      const request = LLM.request({ model: resolved, prompt: "Hello" })
+      const headers = yield* resolved.route.auth.apply({
+        request,
+        method: "POST",
+        url: "https://chatgpt.com/backend-api/codex/responses",
+        body: "{}",
+        headers: Headers.empty,
+      })
+
+      expect(resolved.route.endpoint.baseURL).toBe("https://chatgpt.com/backend-api/codex")
+      expect(headers.authorization).toBe("Bearer oauth-token")
+      expect(headers["chatgpt-account-id"]).toBe("account-123")
+    }),
+  )
+
   it.effect("prefers stored credentials over configured auth", () =>
     Effect.gen(function* () {
       const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
@@ -342,6 +412,11 @@ describe("SessionRunnerModel", () => {
         ),
       ).toBe(false)
       expect(SessionRunnerModel.supported(model({ type: "native", settings: {} }))).toBe(false)
+      expect(
+        SessionRunnerModel.supported(
+          model({ type: "native", package: "@opencode-ai/llm/providers/openai", settings: {} }),
+        ),
+      ).toBe(true)
     }),
   )
 })

+ 2 - 0
packages/llm/package.json

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

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

@@ -1,6 +1,7 @@
 export { LLMClient } from "./route/client"
 export { Auth } from "./route/auth"
 export { Provider } from "./provider"
+export { ProviderPackage } from "./provider-package"
 export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
 export type {
   RouteModelInput,

+ 25 - 2
packages/llm/src/protocols/anthropic-messages.ts

@@ -1,11 +1,13 @@
 import { Effect, Schema } from "effect"
-import { Route } from "../route/client"
-import { Auth } from "../route/auth"
+import { Route, type RoutePatch } from "../route/client"
+import { Auth, type Auth as AuthDef } from "../route/auth"
 import { Endpoint } from "../route/endpoint"
 import { Framing } from "../route/framing"
 import { Protocol } from "../route/protocol"
 import {
   LLMEvent,
+  type Model,
+  type ProviderOptions,
   Usage,
   type CacheHint,
   type FinishReason,
@@ -852,4 +854,25 @@ export const route = Route.make({
   headers: () => ({ "anthropic-version": "2023-06-01" }),
 })
 
+export interface ModelConfig {
+  readonly auth: AuthDef
+  readonly baseURL?: string
+  readonly headers?: Readonly<Record<string, string>>
+  readonly providerOptions?: ProviderOptions
+  readonly body?: Readonly<Record<string, unknown>>
+  readonly limits?: { readonly context: number; readonly output: number }
+}
+
+export const model = (id: string, config: ModelConfig): Model =>
+  route
+    .with({
+      auth: config.auth,
+      endpoint: config.baseURL === undefined ? undefined : { baseURL: config.baseURL },
+      headers: config.headers,
+      providerOptions: config.providerOptions,
+      http: config.body === undefined ? undefined : { body: config.body },
+      limits: config.limits,
+    } satisfies RoutePatch<AnthropicMessagesBody, unknown>)
+    .model({ id })
+
 export * as AnthropicMessages from "./anthropic-messages"

+ 24 - 1
packages/llm/src/protocols/openai-compatible-chat.ts

@@ -1,6 +1,8 @@
-import { Route, type RouteRoutedModelInput } from "../route/client"
+import { Route, type RoutePatch, type RouteRoutedModelInput } from "../route/client"
+import type { Auth as AuthDef } from "../route/auth"
 import { Endpoint } from "../route/endpoint"
 import { Framing } from "../route/framing"
+import type { Model, ProviderOptions } from "../schema"
 import * as OpenAIChat from "./openai-chat"
 
 const ADAPTER = "openai-compatible-chat"
@@ -21,4 +23,25 @@ export const route = Route.make({
   framing: Framing.sse,
 })
 
+export interface ModelConfig {
+  readonly auth: AuthDef
+  readonly baseURL?: string
+  readonly headers?: Readonly<Record<string, string>>
+  readonly providerOptions?: ProviderOptions
+  readonly body?: Readonly<Record<string, unknown>>
+  readonly limits?: { readonly context: number; readonly output: number }
+}
+
+export const model = (id: string, config: ModelConfig): Model =>
+  route
+    .with({
+      auth: config.auth,
+      endpoint: config.baseURL === undefined ? undefined : { baseURL: config.baseURL },
+      headers: config.headers,
+      providerOptions: config.providerOptions,
+      http: config.body === undefined ? undefined : { body: config.body },
+      limits: config.limits,
+    } satisfies RoutePatch<OpenAIChat.OpenAIChatBody, unknown>)
+    .model({ id, provider: "openai-compatible" })
+
 export * as OpenAICompatibleChat from "./openai-compatible-chat"

+ 25 - 2
packages/llm/src/protocols/openai-responses.ts

@@ -1,11 +1,13 @@
 import { Effect, Schema } from "effect"
-import { Route } from "../route/client"
-import { Auth } from "../route/auth"
+import { Route, type RoutePatch } from "../route/client"
+import { Auth, type Auth as AuthDef } from "../route/auth"
 import { Endpoint } from "../route/endpoint"
 import { HttpTransport, WebSocketTransport } from "../route/transport"
 import { Protocol } from "../route/protocol"
 import {
   LLMEvent,
+  type Model,
+  type ProviderOptions,
   Usage,
   type FinishReason,
   type JsonSchema,
@@ -991,6 +993,27 @@ export const route = Route.make({
   defaults: { providerOptions: { openai: { store: false } } },
 })
 
+export interface ModelConfig {
+  readonly auth: AuthDef
+  readonly baseURL?: string
+  readonly headers?: Readonly<Record<string, string>>
+  readonly providerOptions?: ProviderOptions
+  readonly body?: Readonly<Record<string, unknown>>
+  readonly limits?: { readonly context: number; readonly output: number }
+}
+
+export const model = (id: string, config: ModelConfig): Model =>
+  route
+    .with({
+      auth: config.auth,
+      endpoint: config.baseURL === undefined ? undefined : { baseURL: config.baseURL },
+      headers: config.headers,
+      providerOptions: config.providerOptions,
+      http: config.body === undefined ? undefined : { body: config.body },
+      limits: config.limits,
+    } satisfies RoutePatch<OpenAIResponsesBody, unknown>)
+    .model({ id })
+
 const decodeWebSocketMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesWebSocketMessage))
 
 const webSocketMessage = (body: OpenAIResponsesBody | Record<string, unknown>) =>

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

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

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

@@ -2,6 +2,7 @@ import type { RouteDefaultsInput } from "../route/client"
 import { Auth } from "../route/auth"
 import type { ProviderAuthOption } from "../route/auth-options"
 import { ProviderID, type ModelID } from "../schema"
+import { ProviderPackage } from "../provider-package"
 import * as AnthropicMessages from "../protocols/anthropic-messages"
 
 export const id = ProviderID.make("anthropic")
@@ -10,6 +11,8 @@ export const routes = [AnthropicMessages.route]
 
 export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
 
+export interface AnthropicSettings extends ProviderPackage.Settings {}
+
 const auth = (options: ProviderAuthOption<"optional">) => {
   if ("auth" in options && options.auth) return options.auth
   return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey")
@@ -32,4 +35,13 @@ export const configure = (input: Config = {}) => {
 }
 
 export const provider = configure()
-export const model = provider.model
+export const model = ProviderPackage.define((modelID, settings: AnthropicSettings) =>
+  AnthropicMessages.model(modelID, {
+    auth: settings.apiKey === undefined ? Auth.none : Auth.header("x-api-key", settings.apiKey),
+    baseURL: settings.baseURL,
+    headers: settings.headers,
+    providerOptions: settings.providerOptions === undefined ? undefined : { anthropic: settings.providerOptions },
+    body: settings.body,
+    limits: settings.limits,
+  }),
+)

+ 1 - 0
packages/llm/src/providers/index.ts

@@ -6,6 +6,7 @@ export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"
 export * as GitHubCopilot from "./github-copilot"
 export * as Google from "./google"
 export * as OpenAI from "./openai"
+export * as OpenAICodex from "./openai/codex"
 export * as OpenAICompatible from "./openai-compatible"
 export * as OpenRouter from "./openrouter"
 export * as XAI from "./xai"

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

@@ -2,6 +2,8 @@ import { ProviderID, type ModelID } from "../schema"
 import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
 import type { RouteDefaultsInput } from "../route/client"
 import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
+import { Auth } from "../route/auth"
+import { ProviderPackage } from "../provider-package"
 import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
 
 export const id = ProviderID.make("openai-compatible")
@@ -17,6 +19,8 @@ export type FamilyModelOptions = RouteDefaultsInput &
     readonly baseURL?: string
   }
 
+export interface OpenAICompatibleSettings extends ProviderPackage.Settings {}
+
 export const routes = [OpenAICompatibleChat.route]
 
 export const configure = (input: GenericModelOptions) => {
@@ -56,6 +60,17 @@ export const provider = {
   configure,
 }
 
+export const model = ProviderPackage.define((modelID, settings: OpenAICompatibleSettings) =>
+  OpenAICompatibleChat.model(modelID, {
+    auth: settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey),
+    baseURL: settings.baseURL,
+    headers: settings.headers,
+    providerOptions: settings.providerOptions === undefined ? undefined : { openai: settings.providerOptions },
+    body: settings.body,
+    limits: settings.limits,
+  }),
+)
+
 export const baseten = define(profiles.baseten)
 export const cerebras = define(profiles.cerebras)
 export const deepinfra = define(profiles.deepinfra)

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

@@ -1,6 +1,8 @@
 import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
+import { Auth } from "../route/auth"
 import type { Route, RouteDefaultsInput } from "../route/client"
 import { ProviderID, type ModelID } from "../schema"
+import { ProviderPackage } from "../provider-package"
 import * as OpenAIChat from "../protocols/openai-chat"
 import * as OpenAIResponses from "../protocols/openai-responses"
 import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
@@ -21,6 +23,8 @@ export type Config = RouteDefaultsInput &
     readonly providerOptions?: OpenAIProviderOptionsInput
   }
 
+export interface OpenAISettings extends ProviderPackage.Settings {}
+
 const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
 
 const defaults = (input: Config) => {
@@ -57,7 +61,16 @@ export const configure = (input: Config = {}) => {
 
 export const provider = configure()
 
-export const model = provider.model
+export const model = ProviderPackage.define((modelID, settings: OpenAISettings) =>
+  OpenAIResponses.model(modelID, {
+    auth: settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey),
+    baseURL: settings.baseURL,
+    headers: settings.headers,
+    providerOptions: settings.providerOptions === undefined ? undefined : { openai: settings.providerOptions },
+    body: settings.body,
+    limits: settings.limits,
+  }),
+)
 export const responses = provider.responses
 export const responsesWebSocket = provider.responsesWebSocket
 export const chat = provider.chat

+ 20 - 0
packages/llm/src/providers/openai/codex.ts

@@ -0,0 +1,20 @@
+import { Auth } from "../../route/auth"
+import { ProviderPackage } from "../../provider-package"
+import { OpenAIResponses } from "../../protocols/openai-responses"
+
+export interface OpenAICodexSettings extends ProviderPackage.Settings {
+  readonly accountID?: string
+}
+
+export const model = ProviderPackage.define((modelID, settings: OpenAICodexSettings) =>
+  OpenAIResponses.model(modelID, {
+    auth: (settings.apiKey === undefined ? Auth.none : Auth.bearer(settings.apiKey)).andThen(
+      settings.accountID === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": settings.accountID }),
+    ),
+    baseURL: "https://chatgpt.com/backend-api/codex",
+    headers: settings.headers,
+    providerOptions: settings.providerOptions === undefined ? undefined : { openai: settings.providerOptions },
+    body: settings.body,
+    limits: settings.limits,
+  }),
+)

+ 29 - 28
packages/llm/src/schema/messages.ts

@@ -42,40 +42,41 @@ export type MediaPart = Schema.Schema.Type<typeof MediaPart>
 
 export { ToolContent, ToolFileContent, ToolTextContent }
 
+// Standalone schema const so the derived type does not participate in the
+// Object.assign self-reference below; keeps checking order-independent.
+const toolResultValueSchema = Schema.Union([
+  Schema.Struct({
+    type: Schema.Literal("json"),
+    value: Schema.Unknown,
+  }),
+  Schema.Struct({
+    type: Schema.Literal("text"),
+    value: Schema.Unknown,
+  }),
+  Schema.Struct({
+    type: Schema.Literal("error"),
+    value: Schema.Unknown,
+  }),
+  Schema.Struct({
+    type: Schema.Literal("content"),
+    value: Schema.Array(ToolContent),
+  }),
+]).annotate({ identifier: "LLM.ToolResult" })
+
 const isToolResultValue = (value: unknown): value is ToolResultValue =>
   isRecord(value) &&
   (value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
   "value" in value
 
-export const ToolResultValue = Object.assign(
-  Schema.Union([
-    Schema.Struct({
-      type: Schema.Literal("json"),
-      value: Schema.Unknown,
-    }),
-    Schema.Struct({
-      type: Schema.Literal("text"),
-      value: Schema.Unknown,
-    }),
-    Schema.Struct({
-      type: Schema.Literal("error"),
-      value: Schema.Unknown,
-    }),
-    Schema.Struct({
-      type: Schema.Literal("content"),
-      value: Schema.Array(ToolContent),
-    }),
-  ]).annotate({ identifier: "LLM.ToolResult" }),
-  {
-    is: isToolResultValue,
-    make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
-      if (isToolResultValue(value)) return value
-      if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
-      return { type, value }
-    },
+export const ToolResultValue = Object.assign(toolResultValueSchema, {
+  is: isToolResultValue,
+  make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
+    if (isToolResultValue(value)) return value
+    if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
+    return { type, value }
   },
-)
-export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
+})
+export type ToolResultValue = Schema.Schema.Type<typeof toolResultValueSchema>
 
 export interface ToolOutput {
   readonly structured: unknown

+ 7 - 2
packages/llm/test/exports.test.ts

@@ -1,11 +1,13 @@
 import { describe, expect, test } from "bun:test"
-import { LLM, LLMClient, Provider } from "@opencode-ai/llm"
+import { LLM, LLMClient, Provider, ProviderPackage } from "@opencode-ai/llm"
 import { Route, Protocol } from "@opencode-ai/llm/route"
 import { Provider as ProviderSubpath } from "@opencode-ai/llm/provider"
+import { ProviderPackage as ProviderPackageSubpath } from "@opencode-ai/llm/provider-package"
 import {
   CloudflareAIGateway,
   CloudflareWorkersAI,
   OpenAI,
+  OpenAICodex,
   OpenAICompatible,
   OpenRouter,
   XAI,
@@ -21,6 +23,8 @@ describe("public exports", () => {
     expect(LLMClient.layer).toBeDefined()
     expect(Provider.make).toBeFunction()
     expect(ProviderSubpath.make).toBe(Provider.make)
+    expect(ProviderPackage.define).toBeFunction()
+    expect(ProviderPackageSubpath.define).toBe(ProviderPackage.define)
   })
 
   test("route barrel exposes route-authoring APIs", () => {
@@ -30,10 +34,11 @@ describe("public exports", () => {
 
   test("provider barrels expose user-facing facades", () => {
     expect(OpenAI.model).toBeFunction()
-    expect(OpenAI.provider.model).toBe(OpenAI.model)
+    expect(OpenAI.provider.model).toBeFunction()
     expect(OpenAI.provider.responses).toBe(OpenAI.responses)
     expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
     expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()
+    expect(OpenAICodex.model).toBeFunction()
     expect(OpenAICompatible.deepseek.model).toBeFunction()
     expect(CloudflareAIGateway.configure).toBeFunction()
     expect(CloudflareAIGateway.configure({ accountId: "fixture", gatewayApiKey: "fixture" }).model).toBeFunction()

+ 120 - 0
packages/llm/test/provider/provider-package.test.ts

@@ -0,0 +1,120 @@
+import { describe, expect } from "bun:test"
+import { Effect, Schema } from "effect"
+import { HttpClientRequest } from "effect/unstable/http"
+import { LLM, LLMClient } from "../../src"
+import { Anthropic, OpenAI, OpenAICodex } from "../../src/providers"
+import { it } from "../lib/effect"
+import { dynamicResponse } from "../lib/http"
+import { sseEvents } from "../lib/sse"
+
+const JsonRecord = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown))
+const decodeJsonRecord = Schema.decodeUnknownSync(JsonRecord)
+
+const requestFor = (model: ReturnType<typeof OpenAI.model>) =>
+  LLM.request({
+    model,
+    prompt: "Say hello.",
+    cache: "none",
+  })
+
+describe("provider package contract", () => {
+  it.effect("builds OpenAI Responses models from flat settings", () =>
+    Effect.gen(function* () {
+      yield* LLMClient.generate(
+        requestFor(
+          OpenAI.model("gpt-x", {
+            apiKey: "sk-test",
+            headers: { "x-package": "openai" },
+            body: { metadata: { source: "package" } },
+            providerOptions: { store: true },
+            limits: { context: 100, output: 20 },
+          }),
+        ),
+      ).pipe(
+        Effect.provide(
+          dynamicResponse((input) =>
+            Effect.gen(function* () {
+              const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
+              const body = decodeJsonRecord(input.text)
+
+              expect(web.headers.get("authorization")).toBe("Bearer sk-test")
+              expect(web.headers.get("x-package")).toBe("openai")
+              expect(body).toMatchObject({
+                model: "gpt-x",
+                metadata: { source: "package" },
+                store: true,
+                stream: true,
+              })
+              expect(body).not.toHaveProperty("apiKey")
+              return input.respond(sseEvents({ type: "response.completed", response: {} }), {
+                headers: { "content-type": "text/event-stream" },
+              })
+            }),
+          ),
+        ),
+      )
+    }),
+  )
+
+  it.effect("builds Codex models against the ChatGPT Codex endpoint with optional account header", () =>
+    Effect.gen(function* () {
+      yield* LLMClient.generate(
+        requestFor(OpenAICodex.model("gpt-5-codex", { apiKey: "oauth-token", accountID: "account-123" })),
+      ).pipe(
+        Effect.provide(
+          dynamicResponse((input) =>
+            Effect.gen(function* () {
+              const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
+
+              expect(web.url.startsWith("https://chatgpt.com/backend-api/codex")).toBe(true)
+              expect(web.headers.get("authorization")).toBe("Bearer oauth-token")
+              expect(web.headers.get("chatgpt-account-id")).toBe("account-123")
+              return input.respond(sseEvents({ type: "response.completed", response: {} }), {
+                headers: { "content-type": "text/event-stream" },
+              })
+            }),
+          ),
+        ),
+      )
+
+      yield* LLMClient.generate(requestFor(OpenAICodex.model("gpt-5-codex", { apiKey: "oauth-token" }))).pipe(
+        Effect.provide(
+          dynamicResponse((input) =>
+            Effect.gen(function* () {
+              const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
+
+              expect(web.url.startsWith("https://chatgpt.com/backend-api/codex")).toBe(true)
+              expect(web.headers.get("authorization")).toBe("Bearer oauth-token")
+              expect(web.headers.get("chatgpt-account-id")).toBeNull()
+              return input.respond(sseEvents({ type: "response.completed", response: {} }), {
+                headers: { "content-type": "text/event-stream" },
+              })
+            }),
+          ),
+        ),
+      )
+    }),
+  )
+
+  it.effect("builds Anthropic Messages models with x-api-key auth", () =>
+    Effect.gen(function* () {
+      yield* LLMClient.generate(requestFor(Anthropic.model("claude-x", { apiKey: "anthropic-key" }))).pipe(
+        Effect.provide(
+          dynamicResponse((input) =>
+            Effect.gen(function* () {
+              const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
+              const body = decodeJsonRecord(input.text)
+
+              expect(web.headers.get("x-api-key")).toBe("anthropic-key")
+              expect(body).toMatchObject({ model: "claude-x", stream: true })
+              return input.respond(
+                sseEvents({ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }),
+                { headers: { "content-type": "text/event-stream" } },
+              )
+            }),
+          ),
+        ),
+      )
+    }),
+  )
+})

+ 1 - 0
packages/schema/src/provider.ts

@@ -34,6 +34,7 @@ export const AISDK = Schema.Struct({
 export interface Native extends Schema.Schema.Type<typeof Native> {}
 export const Native = Schema.Struct({
   type: Schema.Literal("native"),
+  package: Schema.String.pipe(optional),
   url: Schema.String.pipe(optional),
   settings: Schema.Record(Schema.String, Schema.Unknown),
 }).annotate({ identifier: "Provider.Native" })

+ 2 - 0
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -4779,6 +4779,7 @@ export type ModelApi =
   | {
       id: string
       type: "native"
+      package?: string
       url?: string
       settings: {
         [key: string]: unknown
@@ -4853,6 +4854,7 @@ export type ProviderAisdk = {
 
 export type ProviderNative = {
   type: "native"
+  package?: string
   url?: string
   settings: {
     [key: string]: unknown