@link-assistant/agent 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EXAMPLES.md +383 -0
- package/LICENSE +24 -0
- package/MODELS.md +95 -0
- package/README.md +388 -0
- package/TOOLS.md +134 -0
- package/package.json +89 -0
- package/src/agent/agent.ts +150 -0
- package/src/agent/generate.txt +75 -0
- package/src/auth/index.ts +64 -0
- package/src/bun/index.ts +96 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +119 -0
- package/src/cli/bootstrap.js +41 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/agent.ts +165 -0
- package/src/cli/cmd/cmd.ts +5 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/mcp.ts +80 -0
- package/src/cli/cmd/models.ts +58 -0
- package/src/cli/cmd/run.ts +359 -0
- package/src/cli/cmd/stats.ts +276 -0
- package/src/cli/error.ts +27 -0
- package/src/command/index.ts +73 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/config/config.ts +705 -0
- package/src/config/markdown.ts +41 -0
- package/src/file/ripgrep.ts +391 -0
- package/src/file/time.ts +38 -0
- package/src/file/watcher.ts +75 -0
- package/src/file.ts +6 -0
- package/src/flag/flag.ts +19 -0
- package/src/format/formatter.ts +248 -0
- package/src/format/index.ts +137 -0
- package/src/global/index.ts +52 -0
- package/src/id/id.ts +72 -0
- package/src/index.js +371 -0
- package/src/mcp/index.ts +289 -0
- package/src/patch/index.ts +622 -0
- package/src/project/bootstrap.ts +22 -0
- package/src/project/instance.ts +67 -0
- package/src/project/project.ts +105 -0
- package/src/project/state.ts +65 -0
- package/src/provider/models-macro.ts +11 -0
- package/src/provider/models.ts +98 -0
- package/src/provider/opencode.js +47 -0
- package/src/provider/provider.ts +636 -0
- package/src/provider/transform.ts +241 -0
- package/src/server/project.ts +48 -0
- package/src/server/server.ts +249 -0
- package/src/session/agent.js +204 -0
- package/src/session/compaction.ts +249 -0
- package/src/session/index.ts +380 -0
- package/src/session/message-v2.ts +758 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +356 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/anthropic_spoof.txt +1 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex.txt +318 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/grok-code.txt +1 -0
- package/src/session/prompt/plan.txt +8 -0
- package/src/session/prompt/polaris.txt +107 -0
- package/src/session/prompt/qwen.txt +109 -0
- package/src/session/prompt/summarize-turn.txt +5 -0
- package/src/session/prompt/summarize.txt +10 -0
- package/src/session/prompt/title.txt +25 -0
- package/src/session/prompt.ts +1390 -0
- package/src/session/retry.ts +53 -0
- package/src/session/revert.ts +108 -0
- package/src/session/status.ts +75 -0
- package/src/session/summary.ts +179 -0
- package/src/session/system.ts +138 -0
- package/src/session/todo.ts +36 -0
- package/src/snapshot/index.ts +197 -0
- package/src/storage/storage.ts +226 -0
- package/src/tool/bash.ts +193 -0
- package/src/tool/bash.txt +121 -0
- package/src/tool/batch.ts +173 -0
- package/src/tool/batch.txt +28 -0
- package/src/tool/codesearch.ts +123 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +604 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/glob.ts +65 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +116 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +110 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/patch.ts +188 -0
- package/src/tool/patch.txt +1 -0
- package/src/tool/read.ts +201 -0
- package/src/tool/read.txt +12 -0
- package/src/tool/registry.ts +87 -0
- package/src/tool/task.ts +126 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +39 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +66 -0
- package/src/tool/webfetch.ts +171 -0
- package/src/tool/webfetch.txt +14 -0
- package/src/tool/websearch.ts +133 -0
- package/src/tool/websearch.txt +11 -0
- package/src/tool/write.ts +33 -0
- package/src/tool/write.txt +8 -0
- package/src/util/binary.ts +41 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/error.ts +54 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +69 -0
- package/src/util/fn.ts +11 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +79 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/locale.ts +39 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +177 -0
- package/src/util/queue.ts +19 -0
- package/src/util/rpc.ts +42 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +54 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import z from "zod"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { Config } from "../config/config"
|
|
4
|
+
import { mergeDeep, sortBy } from "remeda"
|
|
5
|
+
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
|
6
|
+
import { Log } from "../util/log"
|
|
7
|
+
import { BunProc } from "../bun"
|
|
8
|
+
import { ModelsDev } from "./models"
|
|
9
|
+
import { NamedError } from "../util/error"
|
|
10
|
+
import { Auth } from "../auth"
|
|
11
|
+
import { Instance } from "../project/instance"
|
|
12
|
+
import { Global } from "../global"
|
|
13
|
+
import { Flag } from "../flag/flag"
|
|
14
|
+
import { iife } from "../util/iife"
|
|
15
|
+
|
|
16
|
+
export namespace Provider {
|
|
17
|
+
const log = Log.create({ service: "provider" })
|
|
18
|
+
|
|
19
|
+
type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
|
|
20
|
+
autoload: boolean
|
|
21
|
+
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
|
22
|
+
options?: Record<string, any>
|
|
23
|
+
}>
|
|
24
|
+
|
|
25
|
+
type Source = "env" | "config" | "custom" | "api"
|
|
26
|
+
|
|
27
|
+
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
|
28
|
+
async anthropic() {
|
|
29
|
+
return {
|
|
30
|
+
autoload: false,
|
|
31
|
+
options: {
|
|
32
|
+
headers: {
|
|
33
|
+
"anthropic-beta":
|
|
34
|
+
"claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async opencode(input) {
|
|
40
|
+
const hasKey = await (async () => {
|
|
41
|
+
if (input.env.some((item) => process.env[item])) return true
|
|
42
|
+
if (await Auth.get(input.id)) return true
|
|
43
|
+
return false
|
|
44
|
+
})()
|
|
45
|
+
|
|
46
|
+
if (!hasKey) {
|
|
47
|
+
for (const [key, value] of Object.entries(input.models)) {
|
|
48
|
+
if (value.cost.input === 0) continue
|
|
49
|
+
delete input.models[key]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
autoload: Object.keys(input.models).length > 0,
|
|
55
|
+
options: hasKey ? {} : { apiKey: "public" },
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
openai: async () => {
|
|
59
|
+
return {
|
|
60
|
+
autoload: false,
|
|
61
|
+
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
|
62
|
+
return sdk.responses(modelID)
|
|
63
|
+
},
|
|
64
|
+
options: {},
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
azure: async () => {
|
|
68
|
+
return {
|
|
69
|
+
autoload: false,
|
|
70
|
+
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
71
|
+
if (options?.["useCompletionUrls"]) {
|
|
72
|
+
return sdk.chat(modelID)
|
|
73
|
+
} else {
|
|
74
|
+
return sdk.responses(modelID)
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
options: {},
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"azure-cognitive-services": async () => {
|
|
81
|
+
const resourceName = process.env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"]
|
|
82
|
+
return {
|
|
83
|
+
autoload: false,
|
|
84
|
+
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
85
|
+
if (options?.["useCompletionUrls"]) {
|
|
86
|
+
return sdk.chat(modelID)
|
|
87
|
+
} else {
|
|
88
|
+
return sdk.responses(modelID)
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
options: {
|
|
92
|
+
baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"amazon-bedrock": async () => {
|
|
97
|
+
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
|
|
98
|
+
return { autoload: false }
|
|
99
|
+
|
|
100
|
+
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
|
101
|
+
|
|
102
|
+
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
|
|
103
|
+
return {
|
|
104
|
+
autoload: true,
|
|
105
|
+
options: {
|
|
106
|
+
region,
|
|
107
|
+
credentialProvider: fromNodeProviderChain(),
|
|
108
|
+
},
|
|
109
|
+
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
|
110
|
+
let regionPrefix = region.split("-")[0]
|
|
111
|
+
|
|
112
|
+
switch (regionPrefix) {
|
|
113
|
+
case "us": {
|
|
114
|
+
const modelRequiresPrefix = [
|
|
115
|
+
"nova-micro",
|
|
116
|
+
"nova-lite",
|
|
117
|
+
"nova-pro",
|
|
118
|
+
"nova-premier",
|
|
119
|
+
"claude",
|
|
120
|
+
"deepseek",
|
|
121
|
+
].some((m) => modelID.includes(m))
|
|
122
|
+
const isGovCloud = region.startsWith("us-gov")
|
|
123
|
+
if (modelRequiresPrefix && !isGovCloud) {
|
|
124
|
+
modelID = `${regionPrefix}.${modelID}`
|
|
125
|
+
}
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
case "eu": {
|
|
129
|
+
const regionRequiresPrefix = [
|
|
130
|
+
"eu-west-1",
|
|
131
|
+
"eu-west-2",
|
|
132
|
+
"eu-west-3",
|
|
133
|
+
"eu-north-1",
|
|
134
|
+
"eu-central-1",
|
|
135
|
+
"eu-south-1",
|
|
136
|
+
"eu-south-2",
|
|
137
|
+
].some((r) => region.includes(r))
|
|
138
|
+
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
|
|
139
|
+
modelID.includes(m),
|
|
140
|
+
)
|
|
141
|
+
if (regionRequiresPrefix && modelRequiresPrefix) {
|
|
142
|
+
modelID = `${regionPrefix}.${modelID}`
|
|
143
|
+
}
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
case "ap": {
|
|
147
|
+
const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
|
|
148
|
+
if (
|
|
149
|
+
isAustraliaRegion &&
|
|
150
|
+
["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
|
|
151
|
+
) {
|
|
152
|
+
regionPrefix = "au"
|
|
153
|
+
modelID = `${regionPrefix}.${modelID}`
|
|
154
|
+
} else {
|
|
155
|
+
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
|
|
156
|
+
modelID.includes(m),
|
|
157
|
+
)
|
|
158
|
+
if (modelRequiresPrefix) {
|
|
159
|
+
regionPrefix = "apac"
|
|
160
|
+
modelID = `${regionPrefix}.${modelID}`
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return sdk.languageModel(modelID)
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
openrouter: async () => {
|
|
172
|
+
return {
|
|
173
|
+
autoload: false,
|
|
174
|
+
options: {
|
|
175
|
+
headers: {
|
|
176
|
+
"HTTP-Referer": "https://opencode.ai/",
|
|
177
|
+
"X-Title": "opencode",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
vercel: async () => {
|
|
183
|
+
return {
|
|
184
|
+
autoload: false,
|
|
185
|
+
options: {
|
|
186
|
+
headers: {
|
|
187
|
+
"http-referer": "https://opencode.ai/",
|
|
188
|
+
"x-title": "opencode",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"google-vertex": async () => {
|
|
194
|
+
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
|
|
195
|
+
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5"
|
|
196
|
+
const autoload = Boolean(project)
|
|
197
|
+
if (!autoload) return { autoload: false }
|
|
198
|
+
return {
|
|
199
|
+
autoload: true,
|
|
200
|
+
options: {
|
|
201
|
+
project,
|
|
202
|
+
location,
|
|
203
|
+
},
|
|
204
|
+
async getModel(sdk: any, modelID: string) {
|
|
205
|
+
const id = String(modelID).trim()
|
|
206
|
+
return sdk.languageModel(id)
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"google-vertex-anthropic": async () => {
|
|
211
|
+
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
|
|
212
|
+
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "global"
|
|
213
|
+
const autoload = Boolean(project)
|
|
214
|
+
if (!autoload) return { autoload: false }
|
|
215
|
+
return {
|
|
216
|
+
autoload: true,
|
|
217
|
+
options: {
|
|
218
|
+
project,
|
|
219
|
+
location,
|
|
220
|
+
},
|
|
221
|
+
async getModel(sdk: any, modelID: string) {
|
|
222
|
+
const id = String(modelID).trim()
|
|
223
|
+
return sdk.languageModel(id)
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
zenmux: async () => {
|
|
228
|
+
return {
|
|
229
|
+
autoload: false,
|
|
230
|
+
options: {
|
|
231
|
+
headers: {
|
|
232
|
+
"HTTP-Referer": "https://opencode.ai/",
|
|
233
|
+
"X-Title": "opencode",
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const state = Instance.state(async () => {
|
|
241
|
+
using _ = log.time("state")
|
|
242
|
+
const config = await Config.get()
|
|
243
|
+
const database = await ModelsDev.get()
|
|
244
|
+
|
|
245
|
+
const providers: {
|
|
246
|
+
[providerID: string]: {
|
|
247
|
+
source: Source
|
|
248
|
+
info: ModelsDev.Provider
|
|
249
|
+
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
|
250
|
+
options: Record<string, any>
|
|
251
|
+
}
|
|
252
|
+
} = {}
|
|
253
|
+
const models = new Map<
|
|
254
|
+
string,
|
|
255
|
+
{
|
|
256
|
+
providerID: string
|
|
257
|
+
modelID: string
|
|
258
|
+
info: ModelsDev.Model
|
|
259
|
+
language: LanguageModel
|
|
260
|
+
npm?: string
|
|
261
|
+
}
|
|
262
|
+
>()
|
|
263
|
+
const sdk = new Map<number, SDK>()
|
|
264
|
+
// Maps `${provider}/${key}` to the provider’s actual model ID for custom aliases.
|
|
265
|
+
const realIdByKey = new Map<string, string>()
|
|
266
|
+
|
|
267
|
+
log.info("init")
|
|
268
|
+
|
|
269
|
+
function mergeProvider(
|
|
270
|
+
id: string,
|
|
271
|
+
options: Record<string, any>,
|
|
272
|
+
source: Source,
|
|
273
|
+
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>,
|
|
274
|
+
) {
|
|
275
|
+
const provider = providers[id]
|
|
276
|
+
if (!provider) {
|
|
277
|
+
const info = database[id]
|
|
278
|
+
if (!info) return
|
|
279
|
+
if (info.api && !options["baseURL"]) options["baseURL"] = info.api
|
|
280
|
+
providers[id] = {
|
|
281
|
+
source,
|
|
282
|
+
info,
|
|
283
|
+
options,
|
|
284
|
+
getModel,
|
|
285
|
+
}
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
provider.options = mergeDeep(provider.options, options)
|
|
289
|
+
provider.source = source
|
|
290
|
+
provider.getModel = getModel ?? provider.getModel
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const configProviders = Object.entries(config.provider ?? {})
|
|
294
|
+
|
|
295
|
+
// Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot
|
|
296
|
+
if (database["github-copilot"]) {
|
|
297
|
+
const githubCopilot = database["github-copilot"]
|
|
298
|
+
database["github-copilot-enterprise"] = {
|
|
299
|
+
...githubCopilot,
|
|
300
|
+
id: "github-copilot-enterprise",
|
|
301
|
+
name: "GitHub Copilot Enterprise",
|
|
302
|
+
// Enterprise uses a different API endpoint - will be set dynamically based on auth
|
|
303
|
+
api: undefined,
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const [providerID, provider] of configProviders) {
|
|
308
|
+
const existing = database[providerID]
|
|
309
|
+
const parsed: ModelsDev.Provider = {
|
|
310
|
+
id: providerID,
|
|
311
|
+
npm: provider.npm ?? existing?.npm,
|
|
312
|
+
name: provider.name ?? existing?.name ?? providerID,
|
|
313
|
+
env: provider.env ?? existing?.env ?? [],
|
|
314
|
+
api: provider.api ?? existing?.api,
|
|
315
|
+
models: existing?.models ?? {},
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
|
|
319
|
+
const existing = parsed.models[model.id ?? modelID]
|
|
320
|
+
const name = iife(() => {
|
|
321
|
+
if (model.name) return model.name
|
|
322
|
+
if (model.id && model.id !== modelID) return modelID
|
|
323
|
+
return existing?.name ?? modelID
|
|
324
|
+
})
|
|
325
|
+
const parsedModel: ModelsDev.Model = {
|
|
326
|
+
id: modelID,
|
|
327
|
+
name,
|
|
328
|
+
release_date: model.release_date ?? existing?.release_date,
|
|
329
|
+
attachment: model.attachment ?? existing?.attachment ?? false,
|
|
330
|
+
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
|
331
|
+
temperature: model.temperature ?? existing?.temperature ?? false,
|
|
332
|
+
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
|
333
|
+
cost:
|
|
334
|
+
!model.cost && !existing?.cost
|
|
335
|
+
? {
|
|
336
|
+
input: 0,
|
|
337
|
+
output: 0,
|
|
338
|
+
cache_read: 0,
|
|
339
|
+
cache_write: 0,
|
|
340
|
+
}
|
|
341
|
+
: {
|
|
342
|
+
cache_read: 0,
|
|
343
|
+
cache_write: 0,
|
|
344
|
+
...existing?.cost,
|
|
345
|
+
...model.cost,
|
|
346
|
+
},
|
|
347
|
+
options: {
|
|
348
|
+
...existing?.options,
|
|
349
|
+
...model.options,
|
|
350
|
+
},
|
|
351
|
+
limit: model.limit ??
|
|
352
|
+
existing?.limit ?? {
|
|
353
|
+
context: 0,
|
|
354
|
+
output: 0,
|
|
355
|
+
},
|
|
356
|
+
modalities: model.modalities ??
|
|
357
|
+
existing?.modalities ?? {
|
|
358
|
+
input: ["text"],
|
|
359
|
+
output: ["text"],
|
|
360
|
+
},
|
|
361
|
+
headers: model.headers,
|
|
362
|
+
provider: model.provider ?? existing?.provider,
|
|
363
|
+
}
|
|
364
|
+
if (model.id && model.id !== modelID) {
|
|
365
|
+
realIdByKey.set(`${providerID}/${modelID}`, model.id)
|
|
366
|
+
}
|
|
367
|
+
parsed.models[modelID] = parsedModel
|
|
368
|
+
}
|
|
369
|
+
database[providerID] = parsed
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? []))
|
|
373
|
+
// load env
|
|
374
|
+
for (const [providerID, provider] of Object.entries(database)) {
|
|
375
|
+
if (disabled.has(providerID)) continue
|
|
376
|
+
const apiKey = provider.env.map((item) => process.env[item]).at(0)
|
|
377
|
+
if (!apiKey) continue
|
|
378
|
+
mergeProvider(
|
|
379
|
+
providerID,
|
|
380
|
+
// only include apiKey if there's only one potential option
|
|
381
|
+
provider.env.length === 1 ? { apiKey } : {},
|
|
382
|
+
"env",
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// load apikeys
|
|
387
|
+
for (const [providerID, provider] of Object.entries(await Auth.all())) {
|
|
388
|
+
if (disabled.has(providerID)) continue
|
|
389
|
+
if (provider.type === "api") {
|
|
390
|
+
mergeProvider(providerID, { apiKey: provider.key }, "api")
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// load custom
|
|
395
|
+
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
|
396
|
+
if (disabled.has(providerID)) continue
|
|
397
|
+
const result = await fn(database[providerID])
|
|
398
|
+
if (result && (result.autoload || providers[providerID])) {
|
|
399
|
+
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// load config
|
|
404
|
+
for (const [providerID, provider] of configProviders) {
|
|
405
|
+
mergeProvider(providerID, provider.options ?? {}, "config")
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (const [providerID, provider] of Object.entries(providers)) {
|
|
409
|
+
const filteredModels = Object.fromEntries(
|
|
410
|
+
Object.entries(provider.info.models)
|
|
411
|
+
// Filter out blacklisted models
|
|
412
|
+
.filter(
|
|
413
|
+
([modelID]) =>
|
|
414
|
+
modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
|
|
415
|
+
)
|
|
416
|
+
// Filter out experimental models
|
|
417
|
+
.filter(
|
|
418
|
+
([, model]) =>
|
|
419
|
+
((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
|
|
420
|
+
model.status !== "deprecated",
|
|
421
|
+
),
|
|
422
|
+
)
|
|
423
|
+
provider.info.models = filteredModels
|
|
424
|
+
|
|
425
|
+
if (Object.keys(provider.info.models).length === 0) {
|
|
426
|
+
delete providers[providerID]
|
|
427
|
+
continue
|
|
428
|
+
}
|
|
429
|
+
log.info("found", { providerID })
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
models,
|
|
434
|
+
providers,
|
|
435
|
+
sdk,
|
|
436
|
+
realIdByKey,
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
export async function list() {
|
|
441
|
+
return state().then((state) => state.providers)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model) {
|
|
445
|
+
return (async () => {
|
|
446
|
+
using _ = log.time("getSDK", {
|
|
447
|
+
providerID: provider.id,
|
|
448
|
+
})
|
|
449
|
+
const s = await state()
|
|
450
|
+
const pkg = model.provider?.npm ?? provider.npm ?? provider.id
|
|
451
|
+
const options = { ...s.providers[provider.id]?.options }
|
|
452
|
+
if (pkg.includes("@ai-sdk/openai-compatible") && options["includeUsage"] === undefined) {
|
|
453
|
+
options["includeUsage"] = true
|
|
454
|
+
}
|
|
455
|
+
const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
|
|
456
|
+
const existing = s.sdk.get(key)
|
|
457
|
+
if (existing) return existing
|
|
458
|
+
|
|
459
|
+
let installedPath: string
|
|
460
|
+
if (!pkg.startsWith("file://")) {
|
|
461
|
+
installedPath = await BunProc.install(pkg, "latest")
|
|
462
|
+
} else {
|
|
463
|
+
log.info("loading local provider", { pkg })
|
|
464
|
+
installedPath = pkg
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// The `google-vertex-anthropic` provider points to the `@ai-sdk/google-vertex` package.
|
|
468
|
+
// Ref: https://github.com/sst/models.dev/blob/0a87de42ab177bebad0620a889e2eb2b4a5dd4ab/providers/google-vertex-anthropic/provider.toml
|
|
469
|
+
// However, the actual export is at the subpath `@ai-sdk/google-vertex/anthropic`.
|
|
470
|
+
// Ref: https://ai-sdk.dev/providers/ai-sdk-providers/google-vertex#google-vertex-anthropic-provider-usage
|
|
471
|
+
// In addition, Bun's dynamic import logic does not support subpath imports,
|
|
472
|
+
// so we patch the import path to load directly from `dist`.
|
|
473
|
+
const modPath =
|
|
474
|
+
provider.id === "google-vertex-anthropic" ? `${installedPath}/dist/anthropic/index.mjs` : installedPath
|
|
475
|
+
const mod = await import(modPath)
|
|
476
|
+
if (options["timeout"] !== undefined && options["timeout"] !== null) {
|
|
477
|
+
// Preserve custom fetch if it exists, wrap it with timeout logic
|
|
478
|
+
const customFetch = options["fetch"]
|
|
479
|
+
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
|
|
480
|
+
const { signal, ...rest } = init ?? {}
|
|
481
|
+
|
|
482
|
+
const signals: AbortSignal[] = []
|
|
483
|
+
if (signal) signals.push(signal)
|
|
484
|
+
if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
|
|
485
|
+
|
|
486
|
+
const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
|
|
487
|
+
|
|
488
|
+
const fetchFn = customFetch ?? fetch
|
|
489
|
+
return fetchFn(input, {
|
|
490
|
+
...rest,
|
|
491
|
+
signal: combined,
|
|
492
|
+
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
|
493
|
+
timeout: false,
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
|
498
|
+
const loaded = fn({
|
|
499
|
+
name: provider.id,
|
|
500
|
+
...options,
|
|
501
|
+
})
|
|
502
|
+
s.sdk.set(key, loaded)
|
|
503
|
+
return loaded as SDK
|
|
504
|
+
})().catch((e) => {
|
|
505
|
+
throw new InitError({ providerID: provider.id }, { cause: e })
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export async function getProvider(providerID: string) {
|
|
510
|
+
return state().then((s) => s.providers[providerID])
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export async function getModel(providerID: string, modelID: string) {
|
|
514
|
+
const key = `${providerID}/${modelID}`
|
|
515
|
+
const s = await state()
|
|
516
|
+
if (s.models.has(key)) return s.models.get(key)!
|
|
517
|
+
|
|
518
|
+
log.info("getModel", {
|
|
519
|
+
providerID,
|
|
520
|
+
modelID,
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
const provider = s.providers[providerID]
|
|
524
|
+
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
|
|
525
|
+
const info = provider.info.models[modelID]
|
|
526
|
+
if (!info) throw new ModelNotFoundError({ providerID, modelID })
|
|
527
|
+
const sdk = await getSDK(provider.info, info)
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
const keyReal = `${providerID}/${modelID}`
|
|
531
|
+
const realID = s.realIdByKey.get(keyReal) ?? info.id
|
|
532
|
+
const language = provider.getModel
|
|
533
|
+
? await provider.getModel(sdk, realID, provider.options)
|
|
534
|
+
: sdk.languageModel(realID)
|
|
535
|
+
log.info("found", { providerID, modelID })
|
|
536
|
+
s.models.set(key, {
|
|
537
|
+
providerID,
|
|
538
|
+
modelID,
|
|
539
|
+
info,
|
|
540
|
+
language,
|
|
541
|
+
npm: info.provider?.npm ?? provider.info.npm,
|
|
542
|
+
})
|
|
543
|
+
return {
|
|
544
|
+
modelID,
|
|
545
|
+
providerID,
|
|
546
|
+
info,
|
|
547
|
+
language,
|
|
548
|
+
npm: info.provider?.npm ?? provider.info.npm,
|
|
549
|
+
}
|
|
550
|
+
} catch (e) {
|
|
551
|
+
if (e instanceof NoSuchModelError)
|
|
552
|
+
throw new ModelNotFoundError(
|
|
553
|
+
{
|
|
554
|
+
modelID: modelID,
|
|
555
|
+
providerID,
|
|
556
|
+
},
|
|
557
|
+
{ cause: e },
|
|
558
|
+
)
|
|
559
|
+
throw e
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export async function getSmallModel(providerID: string) {
|
|
564
|
+
const cfg = await Config.get()
|
|
565
|
+
|
|
566
|
+
if (cfg.small_model) {
|
|
567
|
+
const parsed = parseModel(cfg.small_model)
|
|
568
|
+
return getModel(parsed.providerID, parsed.modelID)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const provider = await state().then((state) => state.providers[providerID])
|
|
572
|
+
if (!provider) return
|
|
573
|
+
let priority = ["claude-haiku-4-5", "claude-haiku-4.5", "3-5-haiku", "3.5-haiku", "gemini-2.5-flash", "gpt-5-nano"]
|
|
574
|
+
// claude-haiku-4.5 is considered a premium model in github copilot, we shouldn't use premium requests for title gen
|
|
575
|
+
if (providerID === "github-copilot") {
|
|
576
|
+
priority = priority.filter((m) => m !== "claude-haiku-4.5")
|
|
577
|
+
}
|
|
578
|
+
if (providerID === "opencode" || providerID === "local") {
|
|
579
|
+
priority = ["gpt-5-nano"]
|
|
580
|
+
}
|
|
581
|
+
for (const item of priority) {
|
|
582
|
+
for (const model of Object.keys(provider.info.models)) {
|
|
583
|
+
if (model.includes(item)) return getModel(providerID, model)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
|
589
|
+
export function sort(models: ModelsDev.Model[]) {
|
|
590
|
+
return sortBy(
|
|
591
|
+
models,
|
|
592
|
+
[(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
|
|
593
|
+
[(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
|
|
594
|
+
[(model) => model.id, "desc"],
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export async function defaultModel() {
|
|
599
|
+
const cfg = await Config.get()
|
|
600
|
+
if (cfg.model) return parseModel(cfg.model)
|
|
601
|
+
|
|
602
|
+
const provider = await list()
|
|
603
|
+
.then((val) => Object.values(val))
|
|
604
|
+
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
|
|
605
|
+
if (!provider) throw new Error("no providers found")
|
|
606
|
+
const [model] = sort(Object.values(provider.info.models))
|
|
607
|
+
if (!model) throw new Error("no models found")
|
|
608
|
+
return {
|
|
609
|
+
providerID: provider.info.id,
|
|
610
|
+
modelID: model.id,
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function parseModel(model: string) {
|
|
615
|
+
const [providerID, ...rest] = model.split("/")
|
|
616
|
+
return {
|
|
617
|
+
providerID: providerID,
|
|
618
|
+
modelID: rest.join("/"),
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export const ModelNotFoundError = NamedError.create(
|
|
623
|
+
"ProviderModelNotFoundError",
|
|
624
|
+
z.object({
|
|
625
|
+
providerID: z.string(),
|
|
626
|
+
modelID: z.string(),
|
|
627
|
+
}),
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
export const InitError = NamedError.create(
|
|
631
|
+
"ProviderInitError",
|
|
632
|
+
z.object({
|
|
633
|
+
providerID: z.string(),
|
|
634
|
+
}),
|
|
635
|
+
)
|
|
636
|
+
}
|