@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.
Files changed (133) hide show
  1. package/EXAMPLES.md +383 -0
  2. package/LICENSE +24 -0
  3. package/MODELS.md +95 -0
  4. package/README.md +388 -0
  5. package/TOOLS.md +134 -0
  6. package/package.json +89 -0
  7. package/src/agent/agent.ts +150 -0
  8. package/src/agent/generate.txt +75 -0
  9. package/src/auth/index.ts +64 -0
  10. package/src/bun/index.ts +96 -0
  11. package/src/bus/global.ts +10 -0
  12. package/src/bus/index.ts +119 -0
  13. package/src/cli/bootstrap.js +41 -0
  14. package/src/cli/bootstrap.ts +17 -0
  15. package/src/cli/cmd/agent.ts +165 -0
  16. package/src/cli/cmd/cmd.ts +5 -0
  17. package/src/cli/cmd/export.ts +88 -0
  18. package/src/cli/cmd/mcp.ts +80 -0
  19. package/src/cli/cmd/models.ts +58 -0
  20. package/src/cli/cmd/run.ts +359 -0
  21. package/src/cli/cmd/stats.ts +276 -0
  22. package/src/cli/error.ts +27 -0
  23. package/src/command/index.ts +73 -0
  24. package/src/command/template/initialize.txt +10 -0
  25. package/src/config/config.ts +705 -0
  26. package/src/config/markdown.ts +41 -0
  27. package/src/file/ripgrep.ts +391 -0
  28. package/src/file/time.ts +38 -0
  29. package/src/file/watcher.ts +75 -0
  30. package/src/file.ts +6 -0
  31. package/src/flag/flag.ts +19 -0
  32. package/src/format/formatter.ts +248 -0
  33. package/src/format/index.ts +137 -0
  34. package/src/global/index.ts +52 -0
  35. package/src/id/id.ts +72 -0
  36. package/src/index.js +371 -0
  37. package/src/mcp/index.ts +289 -0
  38. package/src/patch/index.ts +622 -0
  39. package/src/project/bootstrap.ts +22 -0
  40. package/src/project/instance.ts +67 -0
  41. package/src/project/project.ts +105 -0
  42. package/src/project/state.ts +65 -0
  43. package/src/provider/models-macro.ts +11 -0
  44. package/src/provider/models.ts +98 -0
  45. package/src/provider/opencode.js +47 -0
  46. package/src/provider/provider.ts +636 -0
  47. package/src/provider/transform.ts +241 -0
  48. package/src/server/project.ts +48 -0
  49. package/src/server/server.ts +249 -0
  50. package/src/session/agent.js +204 -0
  51. package/src/session/compaction.ts +249 -0
  52. package/src/session/index.ts +380 -0
  53. package/src/session/message-v2.ts +758 -0
  54. package/src/session/message.ts +189 -0
  55. package/src/session/processor.ts +356 -0
  56. package/src/session/prompt/anthropic-20250930.txt +166 -0
  57. package/src/session/prompt/anthropic.txt +105 -0
  58. package/src/session/prompt/anthropic_spoof.txt +1 -0
  59. package/src/session/prompt/beast.txt +147 -0
  60. package/src/session/prompt/build-switch.txt +5 -0
  61. package/src/session/prompt/codex.txt +318 -0
  62. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  63. package/src/session/prompt/gemini.txt +155 -0
  64. package/src/session/prompt/grok-code.txt +1 -0
  65. package/src/session/prompt/plan.txt +8 -0
  66. package/src/session/prompt/polaris.txt +107 -0
  67. package/src/session/prompt/qwen.txt +109 -0
  68. package/src/session/prompt/summarize-turn.txt +5 -0
  69. package/src/session/prompt/summarize.txt +10 -0
  70. package/src/session/prompt/title.txt +25 -0
  71. package/src/session/prompt.ts +1390 -0
  72. package/src/session/retry.ts +53 -0
  73. package/src/session/revert.ts +108 -0
  74. package/src/session/status.ts +75 -0
  75. package/src/session/summary.ts +179 -0
  76. package/src/session/system.ts +138 -0
  77. package/src/session/todo.ts +36 -0
  78. package/src/snapshot/index.ts +197 -0
  79. package/src/storage/storage.ts +226 -0
  80. package/src/tool/bash.ts +193 -0
  81. package/src/tool/bash.txt +121 -0
  82. package/src/tool/batch.ts +173 -0
  83. package/src/tool/batch.txt +28 -0
  84. package/src/tool/codesearch.ts +123 -0
  85. package/src/tool/codesearch.txt +12 -0
  86. package/src/tool/edit.ts +604 -0
  87. package/src/tool/edit.txt +10 -0
  88. package/src/tool/glob.ts +65 -0
  89. package/src/tool/glob.txt +6 -0
  90. package/src/tool/grep.ts +116 -0
  91. package/src/tool/grep.txt +8 -0
  92. package/src/tool/invalid.ts +17 -0
  93. package/src/tool/ls.ts +110 -0
  94. package/src/tool/ls.txt +1 -0
  95. package/src/tool/multiedit.ts +46 -0
  96. package/src/tool/multiedit.txt +41 -0
  97. package/src/tool/patch.ts +188 -0
  98. package/src/tool/patch.txt +1 -0
  99. package/src/tool/read.ts +201 -0
  100. package/src/tool/read.txt +12 -0
  101. package/src/tool/registry.ts +87 -0
  102. package/src/tool/task.ts +126 -0
  103. package/src/tool/task.txt +60 -0
  104. package/src/tool/todo.ts +39 -0
  105. package/src/tool/todoread.txt +14 -0
  106. package/src/tool/todowrite.txt +167 -0
  107. package/src/tool/tool.ts +66 -0
  108. package/src/tool/webfetch.ts +171 -0
  109. package/src/tool/webfetch.txt +14 -0
  110. package/src/tool/websearch.ts +133 -0
  111. package/src/tool/websearch.txt +11 -0
  112. package/src/tool/write.ts +33 -0
  113. package/src/tool/write.txt +8 -0
  114. package/src/util/binary.ts +41 -0
  115. package/src/util/context.ts +25 -0
  116. package/src/util/defer.ts +12 -0
  117. package/src/util/error.ts +54 -0
  118. package/src/util/eventloop.ts +20 -0
  119. package/src/util/filesystem.ts +69 -0
  120. package/src/util/fn.ts +11 -0
  121. package/src/util/iife.ts +3 -0
  122. package/src/util/keybind.ts +79 -0
  123. package/src/util/lazy.ts +11 -0
  124. package/src/util/locale.ts +39 -0
  125. package/src/util/lock.ts +98 -0
  126. package/src/util/log.ts +177 -0
  127. package/src/util/queue.ts +19 -0
  128. package/src/util/rpc.ts +42 -0
  129. package/src/util/scrap.ts +10 -0
  130. package/src/util/signal.ts +12 -0
  131. package/src/util/timeout.ts +14 -0
  132. package/src/util/token.ts +7 -0
  133. 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
+ }