refactor(provider): effectify Provider service (#20160)

This commit is contained in:
Kit Langton
2026-03-30 21:56:43 -04:00
committed by GitHub
parent a898c2ea3a
commit 3df18dcde1
2 changed files with 604 additions and 541 deletions
+6 -8
View File
@@ -210,15 +210,13 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
- [x] `Vcs``project/vcs.ts` - [x] `Vcs``project/vcs.ts`
- [x] `Worktree``worktree/index.ts` - [x] `Worktree``worktree/index.ts`
Still open and likely worth migrating:
- [x] `Session``session/index.ts` - [x] `Session``session/index.ts`
- [ ] `SessionProcessor`blocked by AI SDK v6 PR (#18433) - [x] `SessionProcessor``session/processor.ts`
- [ ] `SessionPrompt`blocked by AI SDK v6 PR (#18433) - [x] `SessionPrompt``session/prompt.ts`
- [ ] `SessionCompaction`blocked by AI SDK v6 PR (#18433) - [x] `SessionCompaction``session/compaction.ts`
- [ ] `Provider`blocked by AI SDK v6 PR (#18433) - [x] `Provider``provider/provider.ts`
Other services not yet migrated: Still open:
- [ ] `SessionSummary``session/summary.ts` - [ ] `SessionSummary``session/summary.ts`
- [ ] `SessionTodo``session/todo.ts` - [ ] `SessionTodo``session/todo.ts`
@@ -235,7 +233,7 @@ Once individual tools are effectified, change `Tool.Info` (`tool/tool.ts`) so `i
1. Migrate each tool to return Effects 1. Migrate each tool to return Effects
2. Update `Tool.define()` factory to work with Effects 2. Update `Tool.define()` factory to work with Effects
3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing — blocked by AI SDK v6 PR (#18433) 3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing
Individual tools, ordered by value: Individual tools, ordered by value:
+180 -115
View File
@@ -19,6 +19,9 @@ import { iife } from "@/util/iife"
import { Global } from "../global" import { Global } from "../global"
import path from "path" import path from "path"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Effect, Layer, ServiceMap } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
// Direct imports for bundled providers // Direct imports for bundled providers
import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock" import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
@@ -857,6 +860,29 @@ export namespace Provider {
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
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>
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3>
readonly closest: (
providerID: ProviderID,
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 }>
}
interface State {
models: Map<string, LanguageModelV3>
providers: Record<ProviderID, Info>
sdk: Map<string, BundledSDK>
modelLoaders: Record<string, CustomModelLoader>
varsLoaders: Record<string, CustomVarsLoader>
}
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Provider") {}
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
const m: Model = { const m: Model = {
id: ModelID.make(model.id), id: ModelID.make(model.id),
@@ -935,14 +961,21 @@ export namespace Provider {
} }
} }
const state = Instance.state(async () => { const layer: Layer.Layer<Service, never, Config.Service | Auth.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
const auth = yield* Auth.Service
const cache = yield* InstanceState.make<State>(
() => Effect.gen(function* () {
using _ = log.time("state") using _ = log.time("state")
const config = await Config.get() const cfg = yield* config.get()
const modelsDev = await ModelsDev.get() const modelsDev = yield* Effect.promise(() => ModelsDev.get())
const database = mapValues(modelsDev, fromModelsDevProvider) const database = mapValues(modelsDev, fromModelsDevProvider)
const disabled = new Set(config.disabled_providers ?? []) const disabled = new Set(cfg.disabled_providers ?? [])
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null
function isProviderAllowed(providerID: ProviderID): boolean { function isProviderAllowed(providerID: ProviderID): boolean {
if (enabled && !enabled.has(providerID)) return false if (enabled && !enabled.has(providerID)) return false
@@ -965,7 +998,7 @@ export namespace Provider {
log.info("init") log.info("init")
const configProviders = Object.entries(config.provider ?? {}) const configProviders = Object.entries(cfg.provider ?? {})
function mergeProvider(providerID: ProviderID, provider: Partial<Info>) { function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
const existing = providers[providerID] const existing = providers[providerID]
@@ -1021,16 +1054,22 @@ export namespace Provider {
toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true, toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
input: { input: {
text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true, text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true,
audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, audio:
image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false,
video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, image:
model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false,
video:
model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false,
pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
}, },
output: { output: {
text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true, text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
audio: model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false, audio:
image: model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false, model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false,
video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false, image:
model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false,
video:
model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false, pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
}, },
interleaved: model.interleaved ?? false, interleaved: model.interleaved ?? false,
@@ -1077,7 +1116,8 @@ export namespace Provider {
} }
// load apikeys // load apikeys
for (const [id, provider] of Object.entries(await Auth.all())) { const auths = yield* auth.all().pipe(Effect.orDie)
for (const [id, provider] of Object.entries(auths)) {
const providerID = ProviderID.make(id) const providerID = ProviderID.make(id)
if (disabled.has(providerID)) continue if (disabled.has(providerID)) continue
if (provider.type === "api") { if (provider.type === "api") {
@@ -1088,22 +1128,23 @@ export namespace Provider {
} }
} }
for (const plugin of await Plugin.list()) { const plugins = yield* Effect.promise(() => Plugin.list())
for (const plugin of plugins) {
if (!plugin.auth) continue if (!plugin.auth) continue
const providerID = ProviderID.make(plugin.auth.provider) const providerID = ProviderID.make(plugin.auth.provider)
if (disabled.has(providerID)) continue if (disabled.has(providerID)) continue
const auth = await Auth.get(providerID) const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
if (!auth) continue if (!pluginAuth) continue
if (!plugin.auth.loader) continue if (!plugin.auth.loader) continue
if (auth) { const options = yield* Effect.promise(() =>
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider]) plugin.auth!.loader!(() => Auth.get(providerID) as any, database[plugin.auth!.provider]),
)
const opts = options ?? {} const opts = options ?? {}
const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts } const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
mergeProvider(providerID, patch) mergeProvider(providerID, patch)
} }
}
for (const [id, fn] of Object.entries(CUSTOM_LOADERS)) { for (const [id, fn] of Object.entries(CUSTOM_LOADERS)) {
const providerID = ProviderID.make(id) const providerID = ProviderID.make(id)
@@ -1113,13 +1154,15 @@ export namespace Provider {
log.error("Provider does not exist in model list " + providerID) log.error("Provider does not exist in model list " + providerID)
continue continue
} }
const result = await fn(data) const result = yield* Effect.promise(() => fn(data))
if (result && (result.autoload || providers[providerID])) { if (result && (result.autoload || providers[providerID])) {
if (result.getModel) modelLoaders[providerID] = result.getModel if (result.getModel) modelLoaders[providerID] = result.getModel
if (result.vars) varsLoaders[providerID] = result.vars if (result.vars) varsLoaders[providerID] = result.vars
if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels
const opts = result.options ?? {} const opts = result.options ?? {}
const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts } const patch: Partial<Info> = providers[providerID]
? { options: opts }
: { source: "custom", options: opts }
mergeProvider(providerID, patch) mergeProvider(providerID, patch)
} }
} }
@@ -1141,7 +1184,7 @@ export namespace Provider {
continue continue
} }
const configProvider = config.provider?.[providerID] const configProvider = cfg.provider?.[providerID]
for (const [modelID, model] of Object.entries(provider.models)) { for (const [modelID, model] of Object.entries(provider.models)) {
model.api.id = model.api.id ?? model.id ?? modelID model.api.id = model.api.id ?? model.id ?? modelID
@@ -1150,7 +1193,8 @@ export namespace Provider {
(providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat") (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
) )
delete provider.models[modelID] delete provider.models[modelID]
if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID] if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS)
delete provider.models[modelID]
if (model.status === "deprecated") delete provider.models[modelID] if (model.status === "deprecated") delete provider.models[modelID]
if ( if (
(configProvider?.blacklist && configProvider.blacklist.includes(modelID)) || (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
@@ -1160,7 +1204,6 @@ export namespace Provider {
model.variants = mapValues(ProviderTransform.variants(model), (v) => v) model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
// Filter out disabled variants from config
const configVariants = configProvider?.models?.[modelID]?.variants const configVariants = configProvider?.models?.[modelID]?.variants
if (configVariants && model.variants) { if (configVariants && model.variants) {
const merged = mergeDeep(model.variants, configVariants) const merged = mergeDeep(model.variants, configVariants)
@@ -1181,14 +1224,18 @@ export namespace Provider {
const gitlab = ProviderID.make("gitlab") const gitlab = ProviderID.make("gitlab")
if (discoveryLoaders[gitlab] && providers[gitlab]) { if (discoveryLoaders[gitlab] && providers[gitlab]) {
await (async () => { yield* Effect.promise(async () => {
try {
const discovered = await discoveryLoaders[gitlab]() const discovered = await discoveryLoaders[gitlab]()
for (const [modelID, model] of Object.entries(discovered)) { for (const [modelID, model] of Object.entries(discovered)) {
if (!providers[gitlab].models[modelID]) { if (!providers[gitlab].models[modelID]) {
providers[gitlab].models[modelID] = model providers[gitlab].models[modelID] = model
} }
} }
})().catch((e) => log.warn("state discovery error", { id: "gitlab", error: e })) } catch (e) {
log.warn("state discovery error", { id: "gitlab", error: e })
}
})
} }
return { return {
@@ -1198,18 +1245,16 @@ export namespace Provider {
modelLoaders, modelLoaders,
varsLoaders, varsLoaders,
} }
}) }),
)
export async function list() { const list = Effect.fn("Provider.list")(() => InstanceState.use(cache, (s) => s.providers))
return state().then((state) => state.providers)
}
async function getSDK(model: Model) { async function resolveSDK(model: Model, s: State) {
try { try {
using _ = log.time("getSDK", { using _ = log.time("getSDK", {
providerID: model.providerID, providerID: model.providerID,
}) })
const s = await state()
const provider = s.providers[model.providerID] const provider = s.providers[model.providerID]
const options = { ...provider.options } const options = { ...provider.options }
@@ -1226,9 +1271,6 @@ export namespace Provider {
typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url
if (!url) return if (!url) return
// some models/providers have variable urls, ex: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1"
// We track this in models.dev, and then when we are resolving the baseURL
// we need to string replace that literal: "${AZURE_RESOURCE_NAME}"
const loader = s.varsLoaders[model.providerID] const loader = s.varsLoaders[model.providerID]
if (loader) { if (loader) {
const vars = loader(options) const vars = loader(options)
@@ -1268,10 +1310,10 @@ export namespace Provider {
delete options["chunkTimeout"] delete options["chunkTimeout"]
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => { options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
// Preserve custom fetch if it exists, wrap it with timeout logic
const fetchFn = customFetch ?? fetch const fetchFn = customFetch ?? fetch
const opts = init ?? {} const opts = init ?? {}
const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined const chunkAbortCtl =
typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
const signals: AbortSignal[] = [] const signals: AbortSignal[] = []
if (opts.signal) signals.push(opts.signal) if (opts.signal) signals.push(opts.signal)
@@ -1279,13 +1321,11 @@ export namespace Provider {
if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false) if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
signals.push(AbortSignal.timeout(options["timeout"])) signals.push(AbortSignal.timeout(options["timeout"]))
const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals) const combined =
signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
if (combined) opts.signal = combined if (combined) opts.signal = combined
// Strip openai itemId metadata following what codex does // Strip openai itemId metadata following what codex does
// Codex uses #[serde(skip_serializing)] on id fields for all item types:
// Message, Reasoning, FunctionCall, LocalShellCall, CustomToolCall, WebSearchCall
// IDs are only re-attached for Azure with store=true
if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") { if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") {
const body = JSON.parse(opts.body as string) const body = JSON.parse(opts.body as string)
const isAzure = model.providerID.includes("azure") const isAzure = model.providerID.includes("azure")
@@ -1346,43 +1386,36 @@ export namespace Provider {
} }
} }
export async function getProvider(providerID: ProviderID) { const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
return state().then((s) => s.providers[providerID]) InstanceState.use(cache, (s) => s.providers[providerID]),
} )
export async function getModel(providerID: ProviderID, modelID: ModelID) { const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
const s = await state() const s = yield* InstanceState.get(cache)
const provider = s.providers[providerID] const provider = s.providers[providerID]
if (!provider) { if (!provider) {
const availableProviders = Object.keys(s.providers) const available = Object.keys(s.providers)
const matches = fuzzysort.go(providerID, availableProviders, { const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 })
limit: 3, throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
threshold: -10000,
})
const suggestions = matches.map((m) => m.target)
throw new ModelNotFoundError({ providerID, modelID, suggestions })
} }
const info = provider.models[modelID] const info = provider.models[modelID]
if (!info) { if (!info) {
const availableModels = Object.keys(provider.models) const available = Object.keys(provider.models)
const matches = fuzzysort.go(modelID, availableModels, { const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 })
limit: 3, throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
threshold: -10000,
})
const suggestions = matches.map((m) => m.target)
throw new ModelNotFoundError({ providerID, modelID, suggestions })
} }
return info return info
} })
export async function getLanguage(model: Model): Promise<LanguageModelV3> { const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) {
const s = await state() const s = yield* InstanceState.get(cache)
const key = `${model.providerID}/${model.id}` const key = `${model.providerID}/${model.id}`
if (s.models.has(key)) return s.models.get(key)! if (s.models.has(key)) return s.models.get(key)!
return yield* Effect.promise(async () => {
const provider = s.providers[model.providerID] const provider = s.providers[model.providerID]
const sdk = await getSDK(model) const sdk = await resolveSDK(model, s)
try { try {
const language = s.modelLoaders[model.providerID] const language = s.modelLoaders[model.providerID]
@@ -1404,33 +1437,33 @@ export namespace Provider {
) )
throw e throw e
} }
} })
})
export async function closest(providerID: ProviderID, query: string[]) { const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
const s = await state() const s = yield* InstanceState.get(cache)
const provider = s.providers[providerID] const provider = s.providers[providerID]
if (!provider) return undefined if (!provider) return undefined
for (const item of query) { for (const item of query) {
for (const modelID of Object.keys(provider.models)) { for (const modelID of Object.keys(provider.models)) {
if (modelID.includes(item)) if (modelID.includes(item)) return { providerID, modelID }
return {
providerID,
modelID,
}
}
} }
} }
return undefined
})
export async function getSmallModel(providerID: ProviderID) { const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
const cfg = await Config.get() const cfg = yield* config.get()
if (cfg.small_model) { if (cfg.small_model) {
const parsed = parseModel(cfg.small_model) const parsed = parseModel(cfg.small_model)
return getModel(parsed.providerID, parsed.modelID) return yield* getModel(parsed.providerID, parsed.modelID)
} }
const provider = await state().then((state) => state.providers[providerID]) const s = yield* InstanceState.get(cache)
if (provider) { const provider = s.providers[providerID]
if (!provider) return undefined
let priority = [ let priority = [
"claude-haiku-4-5", "claude-haiku-4-5",
"claude-haiku-4.5", "claude-haiku-4.5",
@@ -1444,7 +1477,6 @@ export namespace Provider {
priority = ["gpt-5-nano"] priority = ["gpt-5-nano"]
} }
if (providerID.startsWith("github-copilot")) { if (providerID.startsWith("github-copilot")) {
// prioritize free models for github copilot
priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority] priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
} }
for (const item of priority) { for (const item of priority) {
@@ -1452,33 +1484,93 @@ export namespace Provider {
const crossRegionPrefixes = ["global.", "us.", "eu."] const crossRegionPrefixes = ["global.", "us.", "eu."]
const candidates = Object.keys(provider.models).filter((m) => m.includes(item)) const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
// Model selection priority:
// 1. global. prefix (works everywhere)
// 2. User's region prefix (us., eu.)
// 3. Unprefixed model
const globalMatch = candidates.find((m) => m.startsWith("global.")) const globalMatch = candidates.find((m) => m.startsWith("global."))
if (globalMatch) return getModel(providerID, ModelID.make(globalMatch)) if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch))
const region = provider.options?.region const region = provider.options?.region
if (region) { if (region) {
const regionPrefix = region.split("-")[0] const regionPrefix = region.split("-")[0]
if (regionPrefix === "us" || regionPrefix === "eu") { if (regionPrefix === "us" || regionPrefix === "eu") {
const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`)) const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
if (regionalMatch) return getModel(providerID, ModelID.make(regionalMatch)) if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch))
} }
} }
const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p))) const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
if (unprefixed) return getModel(providerID, ModelID.make(unprefixed)) if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed))
} else { } else {
for (const model of Object.keys(provider.models)) { for (const model of Object.keys(provider.models)) {
if (model.includes(item)) return getModel(providerID, ModelID.make(model)) if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model))
}
} }
} }
} }
return undefined return undefined
})
const defaultModel = Effect.fn("Provider.defaultModel")(function* () {
const cfg = yield* config.get()
if (cfg.model) return parseModel(cfg.model)
const s = yield* InstanceState.get(cache)
const recent = yield* Effect.promise(() =>
Filesystem.readJson<{
recent?: { providerID: ProviderID; modelID: ModelID }[]
}>(path.join(Global.Path.state, "model.json"))
.then((x): { providerID: ProviderID; modelID: ModelID }[] => (Array.isArray(x.recent) ? x.recent : []))
.catch((): { providerID: ProviderID; modelID: ModelID }[] => []),
)
for (const entry of recent) {
const provider = s.providers[entry.providerID]
if (!provider) continue
if (!provider.models[entry.modelID]) continue
return { providerID: entry.providerID, modelID: entry.modelID }
}
const provider = Object.values(s.providers).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.models))
if (!model) throw new Error("no models found")
return {
providerID: provider.id,
modelID: model.id,
}
})
return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel })
}),
)
const { runPromise } = makeRuntime(Service, layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Auth.defaultLayer)))
export async function list() {
return runPromise((svc) => svc.list())
}
export async function getProvider(providerID: ProviderID) {
return runPromise((svc) => svc.getProvider(providerID))
}
export async function getModel(providerID: ProviderID, modelID: ModelID) {
return runPromise((svc) => svc.getModel(providerID, modelID))
}
export async function getLanguage(model: Model) {
return runPromise((svc) => svc.getLanguage(model))
}
export async function closest(providerID: ProviderID, query: string[]) {
return runPromise((svc) => svc.closest(providerID, query))
}
export async function getSmallModel(providerID: ProviderID) {
return runPromise((svc) => svc.getSmallModel(providerID))
}
export async function defaultModel() {
return runPromise((svc) => svc.defaultModel())
} }
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
@@ -1491,33 +1583,6 @@ export namespace Provider {
) )
} }
export async function defaultModel() {
const cfg = await Config.get()
if (cfg.model) return parseModel(cfg.model)
const providers = await list()
const recent = (await Filesystem.readJson<{
recent?: { providerID: ProviderID; modelID: ModelID }[]
}>(path.join(Global.Path.state, "model.json"))
.then((x) => (Array.isArray(x.recent) ? x.recent : []))
.catch(() => [])) as { providerID: ProviderID; modelID: ModelID }[]
for (const entry of recent) {
const provider = providers[entry.providerID]
if (!provider) continue
if (!provider.models[entry.modelID]) continue
return { providerID: entry.providerID, modelID: entry.modelID }
}
const provider = Object.values(providers).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.models))
if (!model) throw new Error("no models found")
return {
providerID: provider.id,
modelID: model.id,
}
}
export function parseModel(model: string) { export function parseModel(model: string) {
const [providerID, ...rest] = model.split("/") const [providerID, ...rest] = model.split("/")
return { return {