|
|
|
@@ -25,7 +25,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|
|
|
|
import { isRecord } from "@/util/record"
|
|
|
|
|
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
|
|
|
|
|
import * as ProviderTransform from "./transform"
|
|
|
|
|
import { ModelID, ProviderID } from "./schema"
|
|
|
|
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
|
|
|
|
import { ModelStatus } from "./model-status"
|
|
|
|
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
|
|
|
|
import { ProviderError } from "./error"
|
|
|
|
@@ -663,8 +663,8 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
|
|
|
|
for (const m of result.models) {
|
|
|
|
|
if (!input.models[m.id]) {
|
|
|
|
|
models[m.id] = {
|
|
|
|
|
id: ModelID.make(m.id),
|
|
|
|
|
providerID: ProviderID.make("gitlab"),
|
|
|
|
|
id: ProviderV2.ModelID.make(m.id),
|
|
|
|
|
providerID: ProviderV2.ID.make("gitlab"),
|
|
|
|
|
name: `Agent Platform (${m.name})`,
|
|
|
|
|
family: "",
|
|
|
|
|
api: {
|
|
|
|
@@ -928,8 +928,8 @@ const ProviderLimit = Schema.Struct({
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
export const Model = Schema.Struct({
|
|
|
|
|
id: ModelID,
|
|
|
|
|
providerID: ProviderID,
|
|
|
|
|
id: ProviderV2.ModelID,
|
|
|
|
|
providerID: ProviderV2.ID,
|
|
|
|
|
api: ProviderApiInfo,
|
|
|
|
|
name: Schema.String,
|
|
|
|
|
family: optionalOmitUndefined(Schema.String),
|
|
|
|
@@ -945,7 +945,7 @@ export const Model = Schema.Struct({
|
|
|
|
|
export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
|
|
|
|
|
|
|
|
|
|
export const Info = Schema.Struct({
|
|
|
|
|
id: ProviderID,
|
|
|
|
|
id: ProviderV2.ID,
|
|
|
|
|
name: Schema.String,
|
|
|
|
|
source: Schema.Literals(["env", "config", "custom", "api"]),
|
|
|
|
|
env: Schema.Array(Schema.String),
|
|
|
|
@@ -985,8 +985,8 @@ export function defaultModelIDs<T extends { models: Record<string, { id: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("ProviderModelNotFoundError", {
|
|
|
|
|
providerID: ProviderID,
|
|
|
|
|
modelID: ModelID,
|
|
|
|
|
providerID: ProviderV2.ID,
|
|
|
|
|
modelID: ProviderV2.ModelID,
|
|
|
|
|
suggestions: Schema.optional(Schema.Array(Schema.String)),
|
|
|
|
|
cause: Schema.optional(Schema.Defect),
|
|
|
|
|
}) {
|
|
|
|
@@ -996,7 +996,7 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundErr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class InitError extends Schema.TaggedErrorClass<InitError>()("ProviderInitError", {
|
|
|
|
|
providerID: ProviderID,
|
|
|
|
|
providerID: ProviderV2.ID,
|
|
|
|
|
cause: Schema.optional(Schema.Defect),
|
|
|
|
|
}) {
|
|
|
|
|
static isInstance(input: unknown): input is InitError {
|
|
|
|
@@ -1011,7 +1011,7 @@ export class NoProvidersError extends Schema.TaggedErrorClass<NoProvidersError>(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class NoModelsError extends Schema.TaggedErrorClass<NoModelsError>()("ProviderNoModelsError", {
|
|
|
|
|
providerID: ProviderID,
|
|
|
|
|
providerID: ProviderV2.ID,
|
|
|
|
|
}) {
|
|
|
|
|
static isInstance(input: unknown): input is NoModelsError {
|
|
|
|
|
return input instanceof NoModelsError
|
|
|
|
@@ -1022,22 +1022,22 @@ export type DefaultModelError = ModelNotFoundError | NoProvidersError | NoModels
|
|
|
|
|
export type Error = ModelNotFoundError | InitError | NoProvidersError | NoModelsError
|
|
|
|
|
|
|
|
|
|
export interface Interface {
|
|
|
|
|
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
|
|
|
|
|
readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
|
|
|
|
|
readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model, ModelNotFoundError>
|
|
|
|
|
readonly list: () => Effect.Effect<Record<ProviderV2.ID, Info>>
|
|
|
|
|
readonly getProvider: (providerID: ProviderV2.ID) => Effect.Effect<Info>
|
|
|
|
|
readonly getModel: (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) => Effect.Effect<Model, ModelNotFoundError>
|
|
|
|
|
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
|
|
|
|
|
readonly closest: (
|
|
|
|
|
providerID: ProviderID,
|
|
|
|
|
providerID: ProviderV2.ID,
|
|
|
|
|
query: string[],
|
|
|
|
|
) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined>
|
|
|
|
|
readonly getSmallModel: (providerID: ProviderID) => Effect.Effect<Model | undefined>
|
|
|
|
|
readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }, DefaultModelError>
|
|
|
|
|
) => Effect.Effect<{ providerID: ProviderV2.ID; modelID: string } | undefined>
|
|
|
|
|
readonly getSmallModel: (providerID: ProviderV2.ID) => Effect.Effect<Model | undefined>
|
|
|
|
|
readonly defaultModel: () => Effect.Effect<{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }, DefaultModelError>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
|
models: Map<string, LanguageModelV3>
|
|
|
|
|
providers: Record<ProviderID, Info>
|
|
|
|
|
catalog: Record<ProviderID, Info>
|
|
|
|
|
providers: Record<ProviderV2.ID, Info>
|
|
|
|
|
catalog: Record<ProviderV2.ID, Info>
|
|
|
|
|
sdk: Map<string, BundledSDK>
|
|
|
|
|
modelLoaders: Record<string, CustomModelLoader>
|
|
|
|
|
varsLoaders: Record<string, CustomVarsLoader>
|
|
|
|
@@ -1082,8 +1082,8 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
|
|
|
|
|
|
|
|
|
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
|
|
|
|
const base: Model = {
|
|
|
|
|
id: ModelID.make(model.id),
|
|
|
|
|
providerID: ProviderID.make(provider.id),
|
|
|
|
|
id: ProviderV2.ModelID.make(model.id),
|
|
|
|
|
providerID: ProviderV2.ID.make(provider.id),
|
|
|
|
|
name: model.name,
|
|
|
|
|
family: model.family,
|
|
|
|
|
api: {
|
|
|
|
@@ -1140,7 +1140,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|
|
|
|
const base = fromModelsDevModel(provider, model)
|
|
|
|
|
models[id] = {
|
|
|
|
|
...base,
|
|
|
|
|
id: ModelID.make(id),
|
|
|
|
|
id: ProviderV2.ModelID.make(id),
|
|
|
|
|
name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
|
|
|
|
|
cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
|
|
|
|
|
options: opts.provider?.body
|
|
|
|
@@ -1156,7 +1156,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
id: ProviderID.make(provider.id),
|
|
|
|
|
id: ProviderV2.ID.make(provider.id),
|
|
|
|
|
source: "custom",
|
|
|
|
|
name: provider.name,
|
|
|
|
|
env: [...(provider.env ?? [])],
|
|
|
|
@@ -1175,7 +1175,7 @@ function suggestionModelIDs(provider: Info | undefined, enableExperimentalModels
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function modelSuggestions(provider: Info | undefined, modelID: ModelID, enableExperimentalModels: boolean) {
|
|
|
|
|
function modelSuggestions(provider: Info | undefined, modelID: ProviderV2.ModelID, enableExperimentalModels: boolean) {
|
|
|
|
|
const available = suggestionModelIDs(provider, enableExperimentalModels)
|
|
|
|
|
const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target)
|
|
|
|
|
if (fuzzy.length) return fuzzy
|
|
|
|
@@ -1217,7 +1217,7 @@ export const layer = Layer.effect(
|
|
|
|
|
const catalog = mapValues(modelsDev, fromModelsDevProvider)
|
|
|
|
|
const database = mapValues(catalog, toPublicInfo)
|
|
|
|
|
|
|
|
|
|
const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
|
|
|
|
|
const providers: Record<ProviderV2.ID, Info> = {} as Record<ProviderV2.ID, Info>
|
|
|
|
|
const languages = new Map<string, LanguageModelV3>()
|
|
|
|
|
const modelLoaders: {
|
|
|
|
|
[providerID: string]: CustomModelLoader
|
|
|
|
@@ -1238,7 +1238,7 @@ export const layer = Layer.effect(
|
|
|
|
|
|
|
|
|
|
log.info("init")
|
|
|
|
|
|
|
|
|
|
function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
|
|
|
|
|
function mergeProvider(providerID: ProviderV2.ID, provider: Partial<Info>) {
|
|
|
|
|
const existing = providers[providerID]
|
|
|
|
|
if (existing) {
|
|
|
|
|
// @ts-expect-error
|
|
|
|
@@ -1259,7 +1259,7 @@ export const layer = Layer.effect(
|
|
|
|
|
const disabled = new Set(cfg.disabled_providers ?? [])
|
|
|
|
|
const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null
|
|
|
|
|
|
|
|
|
|
function isProviderAllowed(providerID: ProviderID): boolean {
|
|
|
|
|
function isProviderAllowed(providerID: ProviderV2.ID): boolean {
|
|
|
|
|
if (enabled && !enabled.has(providerID)) return false
|
|
|
|
|
if (disabled.has(providerID)) return false
|
|
|
|
|
return true
|
|
|
|
@@ -1270,7 +1270,7 @@ export const layer = Layer.effect(
|
|
|
|
|
const models = p?.models
|
|
|
|
|
if (!p || !models) continue
|
|
|
|
|
|
|
|
|
|
const providerID = ProviderID.make(p.id)
|
|
|
|
|
const providerID = ProviderV2.ID.make(p.id)
|
|
|
|
|
if (disabled.has(providerID)) continue
|
|
|
|
|
|
|
|
|
|
const provider = database[providerID]
|
|
|
|
@@ -1284,7 +1284,7 @@ export const layer = Layer.effect(
|
|
|
|
|
id,
|
|
|
|
|
{
|
|
|
|
|
...model,
|
|
|
|
|
id: ModelID.make(id),
|
|
|
|
|
id: ProviderV2.ModelID.make(id),
|
|
|
|
|
providerID,
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
@@ -1296,7 +1296,7 @@ export const layer = Layer.effect(
|
|
|
|
|
for (const [providerID, provider] of configProviders) {
|
|
|
|
|
const existing = database[providerID]
|
|
|
|
|
const parsed: Info = {
|
|
|
|
|
id: ProviderID.make(providerID),
|
|
|
|
|
id: ProviderV2.ID.make(providerID),
|
|
|
|
|
name: provider.name ?? existing?.name ?? providerID,
|
|
|
|
|
env: provider.env ?? existing?.env ?? [],
|
|
|
|
|
options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
|
|
|
|
@@ -1319,7 +1319,7 @@ export const layer = Layer.effect(
|
|
|
|
|
return existingModel?.name ?? modelID
|
|
|
|
|
})
|
|
|
|
|
const parsedModel: Model = {
|
|
|
|
|
id: ModelID.make(modelID),
|
|
|
|
|
id: ProviderV2.ModelID.make(modelID),
|
|
|
|
|
api: {
|
|
|
|
|
id: apiID,
|
|
|
|
|
npm: apiNpm,
|
|
|
|
@@ -1327,7 +1327,7 @@ export const layer = Layer.effect(
|
|
|
|
|
},
|
|
|
|
|
status: model.status ?? existingModel?.status ?? "active",
|
|
|
|
|
name,
|
|
|
|
|
providerID: ProviderID.make(providerID),
|
|
|
|
|
providerID: ProviderV2.ID.make(providerID),
|
|
|
|
|
capabilities: {
|
|
|
|
|
temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
|
|
|
|
|
reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
|
|
|
|
@@ -1389,7 +1389,7 @@ export const layer = Layer.effect(
|
|
|
|
|
// load env
|
|
|
|
|
const envs = yield* env.all()
|
|
|
|
|
for (const [id, provider] of Object.entries(database)) {
|
|
|
|
|
const providerID = ProviderID.make(id)
|
|
|
|
|
const providerID = ProviderV2.ID.make(id)
|
|
|
|
|
if (disabled.has(providerID)) continue
|
|
|
|
|
const apiKey = provider.env.map((item) => envs[item]).find(Boolean)
|
|
|
|
|
if (!apiKey) continue
|
|
|
|
@@ -1402,7 +1402,7 @@ export const layer = Layer.effect(
|
|
|
|
|
// load apikeys
|
|
|
|
|
const auths = yield* auth.all().pipe(Effect.orDie)
|
|
|
|
|
for (const [id, provider] of Object.entries(auths)) {
|
|
|
|
|
const providerID = ProviderID.make(id)
|
|
|
|
|
const providerID = ProviderV2.ID.make(id)
|
|
|
|
|
if (disabled.has(providerID)) continue
|
|
|
|
|
if (provider.type === "api") {
|
|
|
|
|
mergeProvider(providerID, {
|
|
|
|
@@ -1415,7 +1415,7 @@ export const layer = Layer.effect(
|
|
|
|
|
// plugin auth loader - database now has entries for config providers
|
|
|
|
|
for (const plugin of plugins) {
|
|
|
|
|
if (!plugin.auth) continue
|
|
|
|
|
const providerID = ProviderID.make(plugin.auth.provider)
|
|
|
|
|
const providerID = ProviderV2.ID.make(plugin.auth.provider)
|
|
|
|
|
if (disabled.has(providerID)) continue
|
|
|
|
|
|
|
|
|
|
const stored = yield* auth.get(providerID).pipe(Effect.orDie)
|
|
|
|
@@ -1434,7 +1434,7 @@ export const layer = Layer.effect(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [id, fn] of Object.entries(custom(dep))) {
|
|
|
|
|
const providerID = ProviderID.make(id)
|
|
|
|
|
const providerID = ProviderV2.ID.make(id)
|
|
|
|
|
if (disabled.has(providerID)) continue
|
|
|
|
|
const data = database[providerID]
|
|
|
|
|
if (!data) {
|
|
|
|
@@ -1454,7 +1454,7 @@ export const layer = Layer.effect(
|
|
|
|
|
|
|
|
|
|
// load config - re-apply with updated data
|
|
|
|
|
for (const [id, provider] of configProviders) {
|
|
|
|
|
const providerID = ProviderID.make(id)
|
|
|
|
|
const providerID = ProviderV2.ID.make(id)
|
|
|
|
|
const partial: Partial<Info> = { source: "config" }
|
|
|
|
|
if (provider.env) partial.env = provider.env
|
|
|
|
|
if (provider.name) partial.name = provider.name
|
|
|
|
@@ -1462,7 +1462,7 @@ export const layer = Layer.effect(
|
|
|
|
|
mergeProvider(providerID, partial)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const gitlab = ProviderID.make("gitlab")
|
|
|
|
|
const gitlab = ProviderV2.ID.make("gitlab")
|
|
|
|
|
if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) {
|
|
|
|
|
yield* Effect.promise(async () => {
|
|
|
|
|
try {
|
|
|
|
@@ -1479,7 +1479,7 @@ export const layer = Layer.effect(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [id, provider] of Object.entries(providers)) {
|
|
|
|
|
const providerID = ProviderID.make(id)
|
|
|
|
|
const providerID = ProviderV2.ID.make(id)
|
|
|
|
|
if (!isProviderAllowed(providerID)) {
|
|
|
|
|
delete providers[providerID]
|
|
|
|
|
continue
|
|
|
|
@@ -1493,10 +1493,10 @@ export const layer = Layer.effect(
|
|
|
|
|
// These chat aliases are invalid for the special handling in the
|
|
|
|
|
// built-in providers below, but custom providers may support them.
|
|
|
|
|
(modelID === "gpt-5-chat-latest" &&
|
|
|
|
|
(providerID === ProviderID.openai ||
|
|
|
|
|
providerID === ProviderID.githubCopilot ||
|
|
|
|
|
providerID === ProviderID.openrouter)) ||
|
|
|
|
|
(providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
|
|
|
|
|
(providerID === ProviderV2.ID.openai ||
|
|
|
|
|
providerID === ProviderV2.ID.githubCopilot ||
|
|
|
|
|
providerID === ProviderV2.ID.openrouter)) ||
|
|
|
|
|
(providerID === ProviderV2.ID.openrouter && modelID === "openai/gpt-5-chat")
|
|
|
|
|
)
|
|
|
|
|
delete provider.models[modelID]
|
|
|
|
|
if (model.status === "alpha" && !runtimeFlags.enableExperimentalModels) delete provider.models[modelID]
|
|
|
|
@@ -1702,11 +1702,11 @@ export const layer = Layer.effect(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
|
|
|
|
|
const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderV2.ID) =>
|
|
|
|
|
InstanceState.use(state, (s) => s.providers[providerID]),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
|
|
|
|
|
const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) {
|
|
|
|
|
const s = yield* InstanceState.get(state)
|
|
|
|
|
const provider = s.providers[providerID]
|
|
|
|
|
if (!provider) {
|
|
|
|
@@ -1756,7 +1756,7 @@ export const layer = Layer.effect(
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
|
|
|
|
|
const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderV2.ID, query: string[]) {
|
|
|
|
|
const s = yield* InstanceState.get(state)
|
|
|
|
|
const provider = s.providers[providerID]
|
|
|
|
|
if (!provider) return undefined
|
|
|
|
@@ -1768,7 +1768,7 @@ export const layer = Layer.effect(
|
|
|
|
|
return undefined
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
|
|
|
|
|
const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderV2.ID) {
|
|
|
|
|
const cfg = yield* config.get()
|
|
|
|
|
|
|
|
|
|
if (cfg.small_model) {
|
|
|
|
@@ -1798,7 +1798,7 @@ export const layer = Layer.effect(
|
|
|
|
|
priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
|
|
|
|
|
}
|
|
|
|
|
for (const item of priority) {
|
|
|
|
|
if (providerID === ProviderID.amazonBedrock) {
|
|
|
|
|
if (providerID === ProviderV2.ID.amazonBedrock) {
|
|
|
|
|
const crossRegionPrefixes = ["global.", "us.", "eu."]
|
|
|
|
|
const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
|
|
|
|
|
|
|
|
|
@@ -1832,16 +1832,16 @@ export const layer = Layer.effect(
|
|
|
|
|
|
|
|
|
|
const s = yield* InstanceState.get(state)
|
|
|
|
|
const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe(
|
|
|
|
|
Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => {
|
|
|
|
|
Effect.map((x): { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }[] => {
|
|
|
|
|
if (!isRecord(x) || !Array.isArray(x.recent)) return []
|
|
|
|
|
return x.recent.flatMap((item) => {
|
|
|
|
|
if (!isRecord(item)) return []
|
|
|
|
|
if (typeof item.providerID !== "string") return []
|
|
|
|
|
if (typeof item.modelID !== "string") return []
|
|
|
|
|
return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }]
|
|
|
|
|
return [{ providerID: ProviderV2.ID.make(item.providerID), modelID: ProviderV2.ModelID.make(item.modelID) }]
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])),
|
|
|
|
|
Effect.catch(() => Effect.succeed([] as { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }[])),
|
|
|
|
|
)
|
|
|
|
|
for (const entry of recent) {
|
|
|
|
|
const provider = s.providers[entry.providerID]
|
|
|
|
@@ -1889,8 +1889,8 @@ export function sort<T extends { id: string }>(models: T[]) {
|
|
|
|
|
export function parseModel(model: string) {
|
|
|
|
|
const [providerID, ...rest] = model.split("/")
|
|
|
|
|
return {
|
|
|
|
|
providerID: ProviderID.make(providerID),
|
|
|
|
|
modelID: ModelID.make(rest.join("/")),
|
|
|
|
|
providerID: ProviderV2.ID.make(providerID),
|
|
|
|
|
modelID: ProviderV2.ModelID.make(rest.join("/")),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|