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
This commit is contained in:
Kit Langton 2026-07-03 14:01:17 -04:00
commit 38398fd450
22 changed files with 634 additions and 114 deletions

View file

@ -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)
}),
)
})