The Atlas doc.haus documentation, bound to its code
108 documents
specs/v2/provider-model.md

The live V2 provider/model schemas: branded ProviderV2.ID with eleven built-in statics, enabled-state unions (env/account/custom), endpoint unions (openai/responses, openai/completions, anthropic/messages, aisdk), tiered ModelV2.Cost with cache pricing, and the Catalog interface (provider.get/all/available, model.default/small). Plugin order runs modelsDev=0 → env=10 → account=20 → provider=30 → config=40 → discovery=50, and the session runner deliberately fails on unsupported endpoint routes instead of silently downgrading. Adding a provider plugin or debugging catalog/model resolution.

Provider and Model Catalog

Provider Schema

export const ID = Schema.String.pipe(
  Schema.brand("ProviderV2.ID"),
  withStatics((schema) => ({
    opencode: schema.make("opencode"),
    anthropic: schema.make("anthropic"),
    openai: schema.make("openai"),
    google: schema.make("google"),
    googleVertex: schema.make("google-vertex"),
    githubCopilot: schema.make("github-copilot"),
    amazonBedrock: schema.make("amazon-bedrock"),
    azure: schema.make("azure"),
    openrouter: schema.make("openrouter"),
    mistral: schema.make("mistral"),
    gitlab: schema.make("gitlab"),
  })),
)
export type ID = typeof ID.Type

const OpenAIResponses = Schema.Struct({
  type: Schema.Literal("openai/responses"),
  url: Schema.String,
  websocket: Schema.optional(Schema.Boolean),
})

const OpenAICompletions = Schema.Struct({
  type: Schema.Literal("openai/completions"),
  url: Schema.String,
  reasoning: Schema.Union([
    Schema.Struct({
      type: Schema.Literal("reasoning_content"),
    }),
    Schema.Struct({
      type: Schema.Literal("reasoning_details"),
    }),
  ]).pipe(Schema.optional),
})
export type OpenAICompletions = typeof OpenAICompletions.Type

const AISDK = Schema.Struct({
  type: Schema.Literal("aisdk"),
  package: Schema.String,
  url: Schema.String.pipe(Schema.optional),
})

const AnthropicMessages = Schema.Struct({
  type: Schema.Literal("anthropic/messages"),
  url: Schema.String,
})

const UnknownEndpoint = Schema.Struct({
  type: Schema.Literal("unknown"),
})

export const Endpoint = Schema.Union([
  UnknownEndpoint,
  OpenAIResponses,
  OpenAICompletions,
  AnthropicMessages,
  AISDK,
]).pipe(Schema.toTaggedUnion("type"))
export type Endpoint = typeof Endpoint.Type

export const Options = Schema.Struct({
  headers: Schema.Record(Schema.String, Schema.String),
  body: Schema.Record(Schema.String, Schema.Any),
  aisdk: Schema.Struct({
    provider: Schema.Record(Schema.String, Schema.Any),
    request: Schema.Record(Schema.String, Schema.Any),
  }),
})
export type Options = typeof Options.Type

export class Info extends Schema.Class<Info>("ProviderV2.Info")({
  id: ID,
  name: Schema.String,
  enabled: Schema.Union([
    Schema.Literal(false),
    Schema.Struct({ via: Schema.Literal("env"), name: Schema.String }),
    Schema.Struct({ via: Schema.Literal("account"), service: Schema.String }),
    Schema.Struct({ via: Schema.Literal("custom"), data: Schema.Record(Schema.String, Schema.Any) }),
  ]),
  env: Schema.String.pipe(Schema.Array),
  endpoint: Endpoint,
  options: Options,
}) {
  static empty(providerID: ID) {
    return new Info({
      id: providerID,
      name: providerID,
      enabled: false,
      env: [],
      endpoint: {
        type: "unknown",
      },
      options: {
        headers: {},
        body: {},
        aisdk: { provider: {}, request: {} },
      },
    })
  }
}

export class NotFound extends Schema.TaggedErrorClass<NotFound>("ProviderV2.NotFound")("ProviderV2.NotFound", {
  providerID: ID,
}) {}

Model Schema

export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID"))
export type ID = typeof ID.Type

export const VariantID = Schema.String.pipe(Schema.brand("VariantID"))
export type VariantID = typeof VariantID.Type

export const Family = Schema.String.pipe(Schema.brand("Family"))
export type Family = typeof Family.Type

export const Capabilities = Schema.Struct({
  tools: Schema.Boolean,
  input: Schema.String.pipe(Schema.Array),
  output: Schema.String.pipe(Schema.Array),
})
export type Capabilities = typeof Capabilities.Type

export const Variant = Schema.Struct({
  id: VariantID,
  ...ProviderV2.Options.fields,
})
export type Variant = typeof Variant.Type

export const Cost = Schema.Struct({
  tier: Schema.Struct({
    type: Schema.Literal("context"),
    size: Schema.Int,
  }).pipe(Schema.optional),
  input: Schema.Finite,
  output: Schema.Finite,
  cache: Schema.Struct({
    read: Schema.Finite,
    write: Schema.Finite,
  }),
})
export type Cost = typeof Cost.Type

export const Limit = Schema.Struct({
  context: Schema.Int,
  input: Schema.Int.pipe(Schema.optional),
  output: Schema.Int,
})
export type Limit = typeof Limit.Type

export const Ref = Schema.Struct({
  id: ID,
  providerID: ProviderV2.ID,
  variant: VariantID.pipe(Schema.optional),
})
export type Ref = typeof Ref.Type

export class Info extends Schema.Class<Info>("ModelV2.Info")({
  id: ID,
  apiID: ID,
  providerID: ProviderV2.ID,
  family: Family.pipe(Schema.optional),
  name: Schema.String,
  endpoint: ProviderV2.Endpoint,
  options: Schema.Struct({
    ...ProviderV2.Options.fields,
    variant: Schema.String.pipe(Schema.optional),
  }),
  capabilities: Capabilities,
  variants: Variant.pipe(Schema.Array),
  time: Schema.Struct({
    released: DateTimeUtcFromMillis,
  }),
  cost: Cost.pipe(Schema.Array),
  status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
  enabled: Schema.Boolean,
  limit: Limit,
}) {
  static empty(providerID: ProviderV2.ID, modelID: ID) {
    return new Info({
      id: modelID,
      apiID: modelID,
      providerID,
      name: modelID,
      endpoint: {
        type: "unknown",
      },
      capabilities: {
        tools: false,
        input: [],
        output: [],
      },
      options: {
        headers: {},
        body: {},
        aisdk: { provider: {}, request: {} },
      },
      variants: [],
      time: {
        released: DateTime.makeUnsafe(0),
      },
      cost: [],
      status: "active",
      enabled: true,
      limit: {
        context: 0,
        output: 0,
      },
    })
  }
}

Catalog Interface

export interface Interface {
  readonly transform: State.Interface<Data, Editor>["transform"]
  readonly provider: {
    readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info, ProviderNotFoundError>
    readonly all: () => Effect.Effect<ProviderV2.Info[]>
    readonly available: () => Effect.Effect<ProviderV2.Info[]>
  }

  readonly model: {
    readonly get: (
      providerID: ProviderV2.ID,
      modelID: ModelV2.ID,
    ) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
    readonly all: () => Effect.Effect<ModelV2.Info[]>
    readonly available: () => Effect.Effect<ModelV2.Info[]>
    readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>
    readonly small: (providerID: ProviderV2.ID) => Effect.Effect<Option.Option<ModelV2.Info>>
  }
}

ProviderV2.Info.enabled is stored provider state. Provider plugins set it to false or record whether availability comes from environment, account, or custom configuration.

ProviderV2.Endpoint includes { type: "unknown" }. CatalogV2.model.get() and CatalogV2.model.all() resolve unknown endpoints from the provider before returning models.

Model storage is nested by provider because model ids are only unique within a provider.

type ProviderRecord = {
  provider: ProviderV2.Info
  models: HashMap.HashMap<ModelV2.ID, ModelV2.Info>
}

let records = HashMap.empty<ProviderV2.ID, ProviderRecord>()

ModelV2.Info.enabled stores model availability. CatalogV2.model.available() also requires a usable provider.

const available = provider.enabled !== false && model.enabled

Current Session Runner Adaptation

The first local V2 Session runner waits for Location plugin boot, then resolves an explicit Session model without silently falling back. Without an explicit model it uses a supported Location catalog default, then falls back to the first available model with a supported route, and otherwise fails with SessionRunnerModel.ModelNotSelectedError. Its native adaptation surface is deliberately narrow:

openai/responses over HTTP
openai/completions for OpenAI Chat
openai/completions for OpenAI-compatible Chat
anthropic/messages
aisdk:@ai-sdk/openai
aisdk:@ai-sdk/openai-compatible with an explicit URL
aisdk:@ai-sdk/anthropic

Native endpoint URLs are complete endpoint URLs and are split into base URL plus request path when building an LLM route. AI SDK endpoint URLs remain base URLs. The adapter preserves model headers and body options, environment-backed provider credentials, direct model API keys, and selected Session variant overlays.

Unsupported routes fail explicitly with SessionRunnerModel.UnsupportedEndpointError. In particular, openai/responses with WebSocket transport must not silently downgrade to HTTP. Google, Azure, Bedrock, OpenRouter-specific behavior, GitHub Copilot, Vertex, gateway adapters, and signed authentication remain future provider slices.

Plugin Interface

type HookSpec = {
  "account.update": {
    input: {
      id: AccountV2.ID
      serviceID: AccountV2.ServiceID
    }
    output: {
      description: string
      credential: AccountV2.Credential
      cancel: boolean
    }
  }

  "account.remove": {
    input: {
      account: AccountV2.Info
    }
    output: {
      cancel: boolean
    }
  }

  "account.activate": {
    input: {}
    output: {
      from?: AccountV2.ID
      to: AccountV2.ID
      cancel: boolean
    }
  }

  "account.activated": {
    input: {
      from?: AccountV2.ID
      to: AccountV2.ID
    }
    output: {}
  }
}

export type Definition<R = never> = Effect.Effect<
  {
    readonly order: number
    readonly hooks: HookFunctions
  },
  never,
  R
>

export interface Interface {
  readonly add: <R = never>(input: { id: ID; definition: Definition<R> }) => Effect.Effect<void, never, R>

  readonly remove: (id: ID) => Effect.Effect<void>

  readonly trigger: <Name extends keyof Hooks>(name: Name, input: HookInput<Name>) => Effect.Effect<HookInput<Name>>
}

Plugin Order

export const Order = {
  modelsDev: 0,
  env: 10,
  account: 20,
  provider: 30,
  config: 40,
  discovery: 50,
} as const

Built-In Plugins

export const ModelsDevPlugin: PluginV2.Definition<ProviderV2.Service | ModelV2.Service | ModelsDev.Service>

export const EnvPlugin: PluginV2.Definition<ProviderV2.Service | Env.Service>

export const AccountPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service>

export const ConfigPlugin: PluginV2.Definition<ProviderV2.Service | ModelV2.Service | Config.Service>

export const AnthropicPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service>

export const OpenRouterPlugin: PluginV2.Definition<ProviderV2.Service>

export const AmazonBedrockPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service | Env.Service>

export const GoogleVertexPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service | Env.Service>

export const GitLabPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service | Env.Service>

export const GitLabDiscoveryPlugin: PluginV2.Definition<ProviderV2.Service | ModelV2.Service | AccountV2.Service>

Plugin Hooks

export type Hooks = {
  init: {}

  "provider.update": {
    provider: Draft<ProviderV2.Info>
    cancel: boolean
  }

  "model.update": {
    model: Draft<ModelV2.Info>
    cancel: boolean
  }
}