Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6a3be31de | ||
|
|
d16c8c9f0f | ||
|
|
fffe20cbe5 | ||
|
|
f6da3c467b | ||
|
|
c0d9f21c0f | ||
|
|
a67b616139 | ||
|
|
715265de4b | ||
|
|
2048f32491 | ||
|
|
2d7ba43a21 | ||
|
|
bd198d8550 | ||
|
|
cbffbcdd3d | ||
|
|
2be8b2269f | ||
|
|
c1fa257a92 | ||
|
|
7fd81dd93e | ||
|
|
d554e7aaef |
@@ -1,11 +1,24 @@
|
|||||||
import { ConfigMarkdown } from "@/config/markdown"
|
import { ConfigMarkdown } from "@/config/markdown"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { MCP } from "../mcp"
|
import { MCP } from "../mcp"
|
||||||
|
import { Provider } from "../provider/provider"
|
||||||
import { UI } from "./ui"
|
import { UI } from "./ui"
|
||||||
|
|
||||||
export function FormatError(input: unknown) {
|
export function FormatError(input: unknown) {
|
||||||
if (MCP.Failed.isInstance(input))
|
if (MCP.Failed.isInstance(input))
|
||||||
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||||
|
if (Provider.ModelNotFoundError.isInstance(input)) {
|
||||||
|
const { providerID, modelID, suggestions } = input.data
|
||||||
|
return [
|
||||||
|
`Model not found: ${providerID}/${modelID}`,
|
||||||
|
...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
|
||||||
|
`Try: \`opencode models\` to list available models`,
|
||||||
|
`Or check your config (opencode.json) provider/model names`,
|
||||||
|
].join("\n")
|
||||||
|
}
|
||||||
|
if (Provider.InitError.isInstance(input)) {
|
||||||
|
return `Failed to initialize provider "${input.data.providerID}". Check credentials and configuration.`
|
||||||
|
}
|
||||||
if (Config.JsonError.isInstance(input)) {
|
if (Config.JsonError.isInstance(input)) {
|
||||||
return (
|
return (
|
||||||
`Config file at ${input.data.path} is not valid JSON(C)` + (input.data.message ? `: ${input.data.message}` : "")
|
`Config file at ${input.data.path} is not valid JSON(C)` + (input.data.message ? `: ${input.data.message}` : "")
|
||||||
|
|||||||
@@ -480,6 +480,10 @@ export namespace Config {
|
|||||||
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
|
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
|
||||||
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
|
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
|
||||||
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
|
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
|
||||||
|
enabled_providers: z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
|
||||||
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||||
small_model: z
|
small_model: z
|
||||||
.string()
|
.string()
|
||||||
@@ -511,6 +515,8 @@ export namespace Config {
|
|||||||
z.string(),
|
z.string(),
|
||||||
ModelsDev.Provider.partial()
|
ModelsDev.Provider.partial()
|
||||||
.extend({
|
.extend({
|
||||||
|
whitelist: z.array(z.string()).optional(),
|
||||||
|
blacklist: z.array(z.string()).optional(),
|
||||||
models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
|
models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
|
||||||
options: z
|
options: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import fuzzysort from "fuzzysort"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { mergeDeep, sortBy } from "remeda"
|
import { mergeDeep, sortBy } from "remeda"
|
||||||
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
||||||
@@ -241,6 +242,15 @@ export namespace Provider {
|
|||||||
const config = await Config.get()
|
const config = await Config.get()
|
||||||
const database = await ModelsDev.get()
|
const database = await ModelsDev.get()
|
||||||
|
|
||||||
|
const disabled = new Set(config.disabled_providers ?? [])
|
||||||
|
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
|
||||||
|
|
||||||
|
function isProviderAllowed(providerID: string): boolean {
|
||||||
|
if (enabled && !enabled.has(providerID)) return false
|
||||||
|
if (disabled.has(providerID)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const providers: {
|
const providers: {
|
||||||
[providerID: string]: {
|
[providerID: string]: {
|
||||||
source: Source
|
source: Source
|
||||||
@@ -365,10 +375,10 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
parsed.models[modelID] = parsedModel
|
parsed.models[modelID] = parsedModel
|
||||||
}
|
}
|
||||||
|
|
||||||
database[providerID] = parsed
|
database[providerID] = parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? []))
|
|
||||||
// load env
|
// load env
|
||||||
for (const [providerID, provider] of Object.entries(database)) {
|
for (const [providerID, provider] of Object.entries(database)) {
|
||||||
if (disabled.has(providerID)) continue
|
if (disabled.has(providerID)) continue
|
||||||
@@ -446,6 +456,12 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [providerID, provider] of Object.entries(providers)) {
|
for (const [providerID, provider] of Object.entries(providers)) {
|
||||||
|
if (!isProviderAllowed(providerID)) {
|
||||||
|
delete providers[providerID]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const configProvider = config.provider?.[providerID]
|
||||||
const filteredModels = Object.fromEntries(
|
const filteredModels = Object.fromEntries(
|
||||||
Object.entries(provider.info.models)
|
Object.entries(provider.info.models)
|
||||||
// Filter out blacklisted models
|
// Filter out blacklisted models
|
||||||
@@ -458,8 +474,18 @@ export namespace Provider {
|
|||||||
([, model]) =>
|
([, model]) =>
|
||||||
((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
|
((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
|
||||||
model.status !== "deprecated",
|
model.status !== "deprecated",
|
||||||
),
|
)
|
||||||
|
// Filter by provider's whitelist/blacklist from config
|
||||||
|
.filter(([modelID]) => {
|
||||||
|
if (!configProvider) return true
|
||||||
|
|
||||||
|
return (
|
||||||
|
(!configProvider.blacklist || !configProvider.blacklist.includes(modelID)) &&
|
||||||
|
(!configProvider.whitelist || configProvider.whitelist.includes(modelID))
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
provider.info.models = filteredModels
|
provider.info.models = filteredModels
|
||||||
|
|
||||||
if (Object.keys(provider.info.models).length === 0) {
|
if (Object.keys(provider.info.models).length === 0) {
|
||||||
@@ -572,9 +598,78 @@ export namespace Provider {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const provider = s.providers[providerID]
|
const provider = s.providers[providerID]
|
||||||
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
|
if (!provider) {
|
||||||
|
let suggestions: string[] = []
|
||||||
|
const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, "")
|
||||||
|
const levenshtein = (a: string, b: string) => {
|
||||||
|
const m = a.length,
|
||||||
|
n = b.length
|
||||||
|
const dp = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))
|
||||||
|
for (let i = 0; i <= m; i++) dp[i][0] = i
|
||||||
|
for (let j = 0; j <= n; j++) dp[0][j] = j
|
||||||
|
for (let i = 1; i <= m; i++) {
|
||||||
|
for (let j = 1; j <= n; j++) {
|
||||||
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
||||||
|
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dp[m][n]
|
||||||
|
}
|
||||||
|
if (!modelID || modelID.trim() === "") {
|
||||||
|
// Treat single-token input as an unqualified model; search across all providers' models.
|
||||||
|
const q = normalize(providerID)
|
||||||
|
const entries: { combo: string; norm: string }[] = []
|
||||||
|
for (const [pid, prov] of Object.entries(s.providers)) {
|
||||||
|
for (const mid of Object.keys(prov.info.models)) {
|
||||||
|
entries.push({ combo: pid + "/" + mid, norm: normalize(mid) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const byNorm = fuzzysort.go(q, entries as any, { limit: 5, key: "norm" }).map((r: any) => r.obj.combo)
|
||||||
|
const combos = entries.map((e) => e.combo)
|
||||||
|
const byRaw = fuzzysort.go(providerID, combos, { limit: 5 }).map((r) => r.target)
|
||||||
|
let merged = Array.from(new Set([...byNorm, ...byRaw]))
|
||||||
|
if (merged.length === 0) {
|
||||||
|
// fallback to edit distance on normalized mid
|
||||||
|
const scored = entries
|
||||||
|
.map((e) => ({ combo: e.combo, d: levenshtein(q, e.norm) }))
|
||||||
|
.sort((a, b) => a.d - b.d)
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((x) => x.combo)
|
||||||
|
merged = scored
|
||||||
|
}
|
||||||
|
suggestions = merged.slice(0, 3)
|
||||||
|
} else {
|
||||||
|
const pcands = Object.keys(s.providers)
|
||||||
|
const corpus = pcands.map((raw) => ({ raw, norm: normalize(raw) }))
|
||||||
|
const q = normalize(providerID)
|
||||||
|
const hits = fuzzysort.go(q, corpus as any, { limit: 5, key: "norm" })
|
||||||
|
let ranked = hits.map((r: any) => r.obj.raw)
|
||||||
|
if (ranked.length === 0) {
|
||||||
|
ranked = pcands
|
||||||
|
.map((p) => ({ p, d: levenshtein(q, normalize(p)) }))
|
||||||
|
.sort((a, b) => a.d - b.d)
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((x) => x.p)
|
||||||
|
}
|
||||||
|
const providerSuggestions = ranked.map((r) => r + "/" + modelID)
|
||||||
|
suggestions = providerSuggestions
|
||||||
|
}
|
||||||
|
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||||
|
}
|
||||||
const info = provider.info.models[modelID]
|
const info = provider.info.models[modelID]
|
||||||
if (!info) throw new ModelNotFoundError({ providerID, modelID })
|
if (!info) {
|
||||||
|
const candidates = Object.keys(provider.info.models)
|
||||||
|
// Normalize punctuation differences like '-' vs '.' by stripping non-alphanumerics
|
||||||
|
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "")
|
||||||
|
const corpus = candidates.map((raw) => ({ raw, norm: normalize(raw) }))
|
||||||
|
const query = normalize(modelID)
|
||||||
|
const results = fuzzysort.go(query, corpus as any, { limit: 5, key: "norm" })
|
||||||
|
const ranked = results.map((r) => ("obj" in r ? (r as any).obj.raw : (r as any).target)) as string[]
|
||||||
|
const fallback = fuzzysort.go(modelID, candidates, { limit: 5 }).map((r) => r.target)
|
||||||
|
const merged = Array.from(new Set([...ranked, ...fallback]))
|
||||||
|
const suggestions = merged.slice(0, 3).map((m) => providerID + "/" + m)
|
||||||
|
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||||
|
}
|
||||||
const sdk = await getSDK(provider.info, info)
|
const sdk = await getSDK(provider.info, info)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -675,6 +770,7 @@ export namespace Provider {
|
|||||||
z.object({
|
z.object({
|
||||||
providerID: z.string(),
|
providerID: z.string(),
|
||||||
modelID: z.string(),
|
modelID: z.string(),
|
||||||
|
suggestions: z.array(z.string()).optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1016,6 +1016,10 @@ export type Config = {
|
|||||||
* Disable providers that are loaded automatically
|
* Disable providers that are loaded automatically
|
||||||
*/
|
*/
|
||||||
disabled_providers?: Array<string>
|
disabled_providers?: Array<string>
|
||||||
|
/**
|
||||||
|
* When set, ONLY these providers will be enabled. All other providers will be ignored
|
||||||
|
*/
|
||||||
|
enabled_providers?: Array<string>
|
||||||
/**
|
/**
|
||||||
* Model to use in the format of provider/model, eg anthropic/claude-2
|
* Model to use in the format of provider/model, eg anthropic/claude-2
|
||||||
*/
|
*/
|
||||||
@@ -1097,6 +1101,8 @@ export type Config = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
whitelist?: Array<string>
|
||||||
|
blacklist?: Array<string>
|
||||||
options?: {
|
options?: {
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
baseURL?: string
|
baseURL?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user