@link-assistant/agent 0.0.8 → 0.0.11

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 (104) hide show
  1. package/EXAMPLES.md +80 -1
  2. package/MODELS.md +72 -24
  3. package/README.md +95 -2
  4. package/TOOLS.md +20 -0
  5. package/package.json +36 -2
  6. package/src/agent/agent.ts +68 -54
  7. package/src/auth/claude-oauth.ts +426 -0
  8. package/src/auth/index.ts +28 -26
  9. package/src/auth/plugins.ts +876 -0
  10. package/src/bun/index.ts +53 -43
  11. package/src/bus/global.ts +5 -5
  12. package/src/bus/index.ts +59 -53
  13. package/src/cli/bootstrap.js +12 -12
  14. package/src/cli/bootstrap.ts +6 -6
  15. package/src/cli/cmd/agent.ts +97 -92
  16. package/src/cli/cmd/auth.ts +468 -0
  17. package/src/cli/cmd/cmd.ts +2 -2
  18. package/src/cli/cmd/export.ts +41 -41
  19. package/src/cli/cmd/mcp.ts +210 -53
  20. package/src/cli/cmd/models.ts +30 -29
  21. package/src/cli/cmd/run.ts +269 -213
  22. package/src/cli/cmd/stats.ts +185 -146
  23. package/src/cli/error.ts +17 -13
  24. package/src/cli/ui.ts +78 -0
  25. package/src/command/index.ts +26 -26
  26. package/src/config/config.ts +528 -288
  27. package/src/config/markdown.ts +15 -15
  28. package/src/file/ripgrep.ts +201 -169
  29. package/src/file/time.ts +21 -18
  30. package/src/file/watcher.ts +51 -42
  31. package/src/file.ts +1 -1
  32. package/src/flag/flag.ts +26 -11
  33. package/src/format/formatter.ts +206 -162
  34. package/src/format/index.ts +61 -61
  35. package/src/global/index.ts +21 -21
  36. package/src/id/id.ts +47 -33
  37. package/src/index.js +554 -332
  38. package/src/json-standard/index.ts +173 -0
  39. package/src/mcp/index.ts +135 -128
  40. package/src/patch/index.ts +336 -267
  41. package/src/project/bootstrap.ts +15 -15
  42. package/src/project/instance.ts +43 -36
  43. package/src/project/project.ts +47 -47
  44. package/src/project/state.ts +37 -33
  45. package/src/provider/models-macro.ts +5 -5
  46. package/src/provider/models.ts +32 -32
  47. package/src/provider/opencode.js +19 -19
  48. package/src/provider/provider.ts +518 -277
  49. package/src/provider/transform.ts +143 -102
  50. package/src/server/project.ts +21 -21
  51. package/src/server/server.ts +111 -105
  52. package/src/session/agent.js +66 -60
  53. package/src/session/compaction.ts +136 -111
  54. package/src/session/index.ts +189 -156
  55. package/src/session/message-v2.ts +312 -268
  56. package/src/session/message.ts +73 -57
  57. package/src/session/processor.ts +180 -166
  58. package/src/session/prompt.ts +678 -533
  59. package/src/session/retry.ts +26 -23
  60. package/src/session/revert.ts +76 -62
  61. package/src/session/status.ts +26 -26
  62. package/src/session/summary.ts +97 -76
  63. package/src/session/system.ts +77 -63
  64. package/src/session/todo.ts +22 -16
  65. package/src/snapshot/index.ts +92 -76
  66. package/src/storage/storage.ts +157 -120
  67. package/src/tool/bash.ts +116 -106
  68. package/src/tool/batch.ts +73 -59
  69. package/src/tool/codesearch.ts +60 -53
  70. package/src/tool/edit.ts +319 -263
  71. package/src/tool/glob.ts +32 -28
  72. package/src/tool/grep.ts +72 -53
  73. package/src/tool/invalid.ts +7 -7
  74. package/src/tool/ls.ts +77 -64
  75. package/src/tool/multiedit.ts +30 -21
  76. package/src/tool/patch.ts +121 -94
  77. package/src/tool/read.ts +140 -122
  78. package/src/tool/registry.ts +38 -38
  79. package/src/tool/task.ts +93 -60
  80. package/src/tool/todo.ts +16 -16
  81. package/src/tool/tool.ts +45 -36
  82. package/src/tool/webfetch.ts +97 -74
  83. package/src/tool/websearch.ts +78 -64
  84. package/src/tool/write.ts +21 -15
  85. package/src/util/binary.ts +27 -19
  86. package/src/util/context.ts +8 -8
  87. package/src/util/defer.ts +7 -5
  88. package/src/util/error.ts +24 -19
  89. package/src/util/eventloop.ts +16 -10
  90. package/src/util/filesystem.ts +37 -33
  91. package/src/util/fn.ts +11 -8
  92. package/src/util/iife.ts +1 -1
  93. package/src/util/keybind.ts +44 -44
  94. package/src/util/lazy.ts +7 -7
  95. package/src/util/locale.ts +20 -16
  96. package/src/util/lock.ts +43 -38
  97. package/src/util/log.ts +95 -85
  98. package/src/util/queue.ts +8 -8
  99. package/src/util/rpc.ts +35 -23
  100. package/src/util/scrap.ts +4 -4
  101. package/src/util/signal.ts +5 -5
  102. package/src/util/timeout.ts +6 -6
  103. package/src/util/token.ts +2 -2
  104. package/src/util/wildcard.ts +38 -27
@@ -1,200 +1,272 @@
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"
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 { ClaudeOAuth } from '../auth/claude-oauth';
12
+ import { AuthPlugins } from '../auth/plugins';
13
+ import { Instance } from '../project/instance';
14
+ import { Global } from '../global';
15
+ import { Flag } from '../flag/flag';
16
+ import { iife } from '../util/iife';
15
17
 
16
18
  export namespace Provider {
17
- const log = Log.create({ service: "provider" })
19
+ const log = Log.create({ service: 'provider' });
18
20
 
19
21
  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
- }>
22
+ autoload: boolean;
23
+ getModel?: (
24
+ sdk: any,
25
+ modelID: string,
26
+ options?: Record<string, any>
27
+ ) => Promise<any>;
28
+ options?: Record<string, any>;
29
+ }>;
24
30
 
25
- type Source = "env" | "config" | "custom" | "api"
31
+ type Source = 'env' | 'config' | 'custom' | 'api';
26
32
 
27
33
  const CUSTOM_LOADERS: Record<string, CustomLoader> = {
28
- async anthropic() {
34
+ async anthropic(input) {
35
+ // Check if OAuth credentials are available via the auth plugin
36
+ const auth = await Auth.get('anthropic');
37
+ if (auth?.type === 'oauth') {
38
+ log.info('using anthropic oauth credentials');
39
+ const loaderFn = await AuthPlugins.getLoader('anthropic');
40
+ if (loaderFn) {
41
+ const result = await loaderFn(() => Auth.get('anthropic'), input);
42
+ if (result.fetch) {
43
+ return {
44
+ autoload: true,
45
+ options: {
46
+ apiKey: result.apiKey || '',
47
+ fetch: result.fetch,
48
+ headers: {
49
+ 'anthropic-beta':
50
+ 'oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14',
51
+ },
52
+ },
53
+ };
54
+ }
55
+ }
56
+ }
57
+ // Default: API key auth
29
58
  return {
30
59
  autoload: false,
31
60
  options: {
32
61
  headers: {
33
- "anthropic-beta":
34
- "claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
62
+ 'anthropic-beta':
63
+ 'claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14',
35
64
  },
36
65
  },
37
- }
66
+ };
38
67
  },
39
68
  async opencode(input) {
40
69
  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
- })()
70
+ if (input.env.some((item) => process.env[item])) return true;
71
+ if (await Auth.get(input.id)) return true;
72
+ return false;
73
+ })();
45
74
 
46
75
  if (!hasKey) {
47
76
  for (const [key, value] of Object.entries(input.models)) {
48
- if (value.cost.input === 0) continue
49
- delete input.models[key]
77
+ if (value.cost.input === 0) continue;
78
+ delete input.models[key];
50
79
  }
51
80
  }
52
81
 
53
82
  return {
54
83
  autoload: Object.keys(input.models).length > 0,
55
- options: hasKey ? {} : { apiKey: "public" },
56
- }
84
+ options: hasKey ? {} : { apiKey: 'public' },
85
+ };
57
86
  },
58
87
  openai: async () => {
59
88
  return {
60
89
  autoload: false,
61
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
62
- return sdk.responses(modelID)
90
+ async getModel(
91
+ sdk: any,
92
+ modelID: string,
93
+ _options?: Record<string, any>
94
+ ) {
95
+ return sdk.responses(modelID);
63
96
  },
64
97
  options: {},
65
- }
98
+ };
66
99
  },
67
100
  azure: async () => {
68
101
  return {
69
102
  autoload: false,
70
- async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
71
- if (options?.["useCompletionUrls"]) {
72
- return sdk.chat(modelID)
103
+ async getModel(
104
+ sdk: any,
105
+ modelID: string,
106
+ options?: Record<string, any>
107
+ ) {
108
+ if (options?.['useCompletionUrls']) {
109
+ return sdk.chat(modelID);
73
110
  } else {
74
- return sdk.responses(modelID)
111
+ return sdk.responses(modelID);
75
112
  }
76
113
  },
77
114
  options: {},
78
- }
115
+ };
79
116
  },
80
- "azure-cognitive-services": async () => {
81
- const resourceName = process.env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"]
117
+ 'azure-cognitive-services': async () => {
118
+ const resourceName =
119
+ process.env['AZURE_COGNITIVE_SERVICES_RESOURCE_NAME'];
82
120
  return {
83
121
  autoload: false,
84
- async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
85
- if (options?.["useCompletionUrls"]) {
86
- return sdk.chat(modelID)
122
+ async getModel(
123
+ sdk: any,
124
+ modelID: string,
125
+ options?: Record<string, any>
126
+ ) {
127
+ if (options?.['useCompletionUrls']) {
128
+ return sdk.chat(modelID);
87
129
  } else {
88
- return sdk.responses(modelID)
130
+ return sdk.responses(modelID);
89
131
  }
90
132
  },
91
133
  options: {
92
- baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
134
+ baseURL: resourceName
135
+ ? `https://${resourceName}.cognitiveservices.azure.com/openai`
136
+ : undefined,
93
137
  },
94
- }
138
+ };
95
139
  },
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 }
140
+ 'amazon-bedrock': async () => {
141
+ if (
142
+ !process.env['AWS_PROFILE'] &&
143
+ !process.env['AWS_ACCESS_KEY_ID'] &&
144
+ !process.env['AWS_BEARER_TOKEN_BEDROCK']
145
+ )
146
+ return { autoload: false };
99
147
 
100
- const region = process.env["AWS_REGION"] ?? "us-east-1"
148
+ const region = process.env['AWS_REGION'] ?? 'us-east-1';
101
149
 
102
- const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
150
+ const { fromNodeProviderChain } = await import(
151
+ await BunProc.install('@aws-sdk/credential-providers')
152
+ );
103
153
  return {
104
154
  autoload: true,
105
155
  options: {
106
156
  region,
107
157
  credentialProvider: fromNodeProviderChain(),
108
158
  },
109
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
110
- let regionPrefix = region.split("-")[0]
159
+ async getModel(
160
+ sdk: any,
161
+ modelID: string,
162
+ _options?: Record<string, any>
163
+ ) {
164
+ let regionPrefix = region.split('-')[0];
111
165
 
112
166
  switch (regionPrefix) {
113
- case "us": {
167
+ case 'us': {
114
168
  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")
169
+ 'nova-micro',
170
+ 'nova-lite',
171
+ 'nova-pro',
172
+ 'nova-premier',
173
+ 'claude',
174
+ 'deepseek',
175
+ ].some((m) => modelID.includes(m));
176
+ const isGovCloud = region.startsWith('us-gov');
123
177
  if (modelRequiresPrefix && !isGovCloud) {
124
- modelID = `${regionPrefix}.${modelID}`
178
+ modelID = `${regionPrefix}.${modelID}`;
125
179
  }
126
- break
180
+ break;
127
181
  }
128
- case "eu": {
182
+ case 'eu': {
129
183
  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
- )
184
+ 'eu-west-1',
185
+ 'eu-west-2',
186
+ 'eu-west-3',
187
+ 'eu-north-1',
188
+ 'eu-central-1',
189
+ 'eu-south-1',
190
+ 'eu-south-2',
191
+ ].some((r) => region.includes(r));
192
+ const modelRequiresPrefix = [
193
+ 'claude',
194
+ 'nova-lite',
195
+ 'nova-micro',
196
+ 'llama3',
197
+ 'pixtral',
198
+ ].some((m) => modelID.includes(m));
141
199
  if (regionRequiresPrefix && modelRequiresPrefix) {
142
- modelID = `${regionPrefix}.${modelID}`
200
+ modelID = `${regionPrefix}.${modelID}`;
143
201
  }
144
- break
202
+ break;
145
203
  }
146
- case "ap": {
147
- const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
204
+ case 'ap': {
205
+ const isAustraliaRegion = [
206
+ 'ap-southeast-2',
207
+ 'ap-southeast-4',
208
+ ].includes(region);
148
209
  if (
149
210
  isAustraliaRegion &&
150
- ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
211
+ ['anthropic.claude-sonnet-4-5', 'anthropic.claude-haiku'].some(
212
+ (m) => modelID.includes(m)
213
+ )
151
214
  ) {
152
- regionPrefix = "au"
153
- modelID = `${regionPrefix}.${modelID}`
215
+ regionPrefix = 'au';
216
+ modelID = `${regionPrefix}.${modelID}`;
154
217
  } else {
155
- const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
156
- modelID.includes(m),
157
- )
218
+ const modelRequiresPrefix = [
219
+ 'claude',
220
+ 'nova-lite',
221
+ 'nova-micro',
222
+ 'nova-pro',
223
+ ].some((m) => modelID.includes(m));
158
224
  if (modelRequiresPrefix) {
159
- regionPrefix = "apac"
160
- modelID = `${regionPrefix}.${modelID}`
225
+ regionPrefix = 'apac';
226
+ modelID = `${regionPrefix}.${modelID}`;
161
227
  }
162
228
  }
163
- break
229
+ break;
164
230
  }
165
231
  }
166
232
 
167
- return sdk.languageModel(modelID)
233
+ return sdk.languageModel(modelID);
168
234
  },
169
- }
235
+ };
170
236
  },
171
237
  openrouter: async () => {
172
238
  return {
173
239
  autoload: false,
174
240
  options: {
175
241
  headers: {
176
- "HTTP-Referer": "https://opencode.ai/",
177
- "X-Title": "opencode",
242
+ 'HTTP-Referer': 'https://opencode.ai/',
243
+ 'X-Title': 'opencode',
178
244
  },
179
245
  },
180
- }
246
+ };
181
247
  },
182
248
  vercel: async () => {
183
249
  return {
184
250
  autoload: false,
185
251
  options: {
186
252
  headers: {
187
- "http-referer": "https://opencode.ai/",
188
- "x-title": "opencode",
253
+ 'http-referer': 'https://opencode.ai/',
254
+ 'x-title': 'opencode',
189
255
  },
190
256
  },
191
- }
257
+ };
192
258
  },
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 }
259
+ 'google-vertex': async () => {
260
+ const project =
261
+ process.env['GOOGLE_CLOUD_PROJECT'] ??
262
+ process.env['GCP_PROJECT'] ??
263
+ process.env['GCLOUD_PROJECT'];
264
+ const location =
265
+ process.env['GOOGLE_CLOUD_LOCATION'] ??
266
+ process.env['VERTEX_LOCATION'] ??
267
+ 'us-east5';
268
+ const autoload = Boolean(project);
269
+ if (!autoload) return { autoload: false };
198
270
  return {
199
271
  autoload: true,
200
272
  options: {
@@ -202,16 +274,22 @@ export namespace Provider {
202
274
  location,
203
275
  },
204
276
  async getModel(sdk: any, modelID: string) {
205
- const id = String(modelID).trim()
206
- return sdk.languageModel(id)
277
+ const id = String(modelID).trim();
278
+ return sdk.languageModel(id);
207
279
  },
208
- }
280
+ };
209
281
  },
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 }
282
+ 'google-vertex-anthropic': async () => {
283
+ const project =
284
+ process.env['GOOGLE_CLOUD_PROJECT'] ??
285
+ process.env['GCP_PROJECT'] ??
286
+ process.env['GCLOUD_PROJECT'];
287
+ const location =
288
+ process.env['GOOGLE_CLOUD_LOCATION'] ??
289
+ process.env['VERTEX_LOCATION'] ??
290
+ 'global';
291
+ const autoload = Boolean(project);
292
+ if (!autoload) return { autoload: false };
215
293
  return {
216
294
  autoload: true,
217
295
  options: {
@@ -219,93 +297,226 @@ export namespace Provider {
219
297
  location,
220
298
  },
221
299
  async getModel(sdk: any, modelID: string) {
222
- const id = String(modelID).trim()
223
- return sdk.languageModel(id)
300
+ const id = String(modelID).trim();
301
+ return sdk.languageModel(id);
224
302
  },
225
- }
303
+ };
226
304
  },
227
305
  zenmux: async () => {
228
306
  return {
229
307
  autoload: false,
230
308
  options: {
231
309
  headers: {
232
- "HTTP-Referer": "https://opencode.ai/",
233
- "X-Title": "opencode",
310
+ 'HTTP-Referer': 'https://opencode.ai/',
311
+ 'X-Title': 'opencode',
234
312
  },
235
313
  },
314
+ };
315
+ },
316
+ groq: async () => {
317
+ return {
318
+ autoload: false,
319
+ options: {},
320
+ };
321
+ },
322
+ /**
323
+ * GitHub Copilot OAuth provider
324
+ * Uses OAuth credentials from agent auth login
325
+ */
326
+ 'github-copilot': async (input) => {
327
+ const auth = await Auth.get('github-copilot');
328
+ if (auth?.type === 'oauth') {
329
+ log.info('using github copilot oauth credentials');
330
+ const loaderFn = await AuthPlugins.getLoader('github-copilot');
331
+ if (loaderFn) {
332
+ const result = await loaderFn(
333
+ () => Auth.get('github-copilot'),
334
+ input
335
+ );
336
+ if (result.fetch) {
337
+ return {
338
+ autoload: true,
339
+ options: {
340
+ apiKey: result.apiKey || '',
341
+ baseURL: result.baseURL,
342
+ fetch: result.fetch,
343
+ },
344
+ };
345
+ }
346
+ }
236
347
  }
348
+ return { autoload: false };
237
349
  },
238
- }
350
+ /**
351
+ * GitHub Copilot Enterprise OAuth provider
352
+ * Uses OAuth credentials from agent auth login with enterprise URL
353
+ */
354
+ 'github-copilot-enterprise': async (input) => {
355
+ const auth = await Auth.get('github-copilot-enterprise');
356
+ if (auth?.type === 'oauth') {
357
+ log.info('using github copilot enterprise oauth credentials');
358
+ const loaderFn = await AuthPlugins.getLoader('github-copilot');
359
+ if (loaderFn) {
360
+ const result = await loaderFn(
361
+ () => Auth.get('github-copilot-enterprise'),
362
+ input
363
+ );
364
+ if (result.fetch) {
365
+ return {
366
+ autoload: true,
367
+ options: {
368
+ apiKey: result.apiKey || '',
369
+ baseURL: result.baseURL,
370
+ fetch: result.fetch,
371
+ },
372
+ };
373
+ }
374
+ }
375
+ }
376
+ return { autoload: false };
377
+ },
378
+ /**
379
+ * Claude OAuth provider - uses Claude OAuth credentials to access
380
+ * Anthropic models via the Claude API.
381
+ *
382
+ * This provider supports two methods:
383
+ * 1. Environment variable: CLAUDE_CODE_OAUTH_TOKEN
384
+ * 2. Credentials file: ~/.claude/.credentials.json (from Claude Code CLI or our auth command)
385
+ *
386
+ * OAuth tokens use Bearer authentication (Authorization header)
387
+ * instead of x-api-key authentication used by standard API keys.
388
+ *
389
+ * To authenticate, run: agent auth claude
390
+ */
391
+ 'claude-oauth': async (input) => {
392
+ // Check for OAuth token from environment variable first
393
+ let oauthToken = process.env['CLAUDE_CODE_OAUTH_TOKEN'];
394
+ let tokenSource = 'environment';
395
+
396
+ if (!oauthToken) {
397
+ // Check for OAuth credentials from credentials file
398
+ const claudeCreds = await ClaudeOAuth.getCredentials();
399
+ if (claudeCreds) {
400
+ oauthToken = claudeCreds.accessToken;
401
+ tokenSource = `credentials file (${claudeCreds.subscriptionType ?? 'unknown'})`;
402
+ }
403
+ }
404
+
405
+ if (!oauthToken) {
406
+ return { autoload: false };
407
+ }
408
+
409
+ log.info('using claude oauth credentials', { source: tokenSource });
410
+
411
+ // Create authenticated fetch with Bearer token and OAuth beta header
412
+ const customFetch = ClaudeOAuth.createAuthenticatedFetch(oauthToken);
413
+
414
+ return {
415
+ autoload: true,
416
+ options: {
417
+ // Use a placeholder key to satisfy the SDK's API key requirement
418
+ // The actual authentication is done via Bearer token in customFetch
419
+ apiKey: 'oauth-token-placeholder',
420
+ fetch: customFetch,
421
+ headers: {
422
+ 'anthropic-beta':
423
+ 'oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14',
424
+ },
425
+ },
426
+ };
427
+ },
428
+ };
239
429
 
240
430
  const state = Instance.state(async () => {
241
- using _ = log.time("state")
242
- const config = await Config.get()
243
- const database = await ModelsDev.get()
431
+ using _ = log.time('state');
432
+ const config = await Config.get();
433
+ const database = await ModelsDev.get();
244
434
 
245
435
  const providers: {
246
436
  [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
- } = {}
437
+ source: Source;
438
+ info: ModelsDev.Provider;
439
+ getModel?: (
440
+ sdk: any,
441
+ modelID: string,
442
+ options?: Record<string, any>
443
+ ) => Promise<any>;
444
+ options: Record<string, any>;
445
+ };
446
+ } = {};
253
447
  const models = new Map<
254
448
  string,
255
449
  {
256
- providerID: string
257
- modelID: string
258
- info: ModelsDev.Model
259
- language: LanguageModel
260
- npm?: string
450
+ providerID: string;
451
+ modelID: string;
452
+ info: ModelsDev.Model;
453
+ language: LanguageModel;
454
+ npm?: string;
261
455
  }
262
- >()
263
- const sdk = new Map<number, SDK>()
456
+ >();
457
+ const sdk = new Map<number, SDK>();
264
458
  // Maps `${provider}/${key}` to the provider’s actual model ID for custom aliases.
265
- const realIdByKey = new Map<string, string>()
459
+ const realIdByKey = new Map<string, string>();
266
460
 
267
- log.info("init")
461
+ log.info('init');
268
462
 
269
463
  function mergeProvider(
270
464
  id: string,
271
465
  options: Record<string, any>,
272
466
  source: Source,
273
- getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>,
467
+ getModel?: (
468
+ sdk: any,
469
+ modelID: string,
470
+ options?: Record<string, any>
471
+ ) => Promise<any>
274
472
  ) {
275
- const provider = providers[id]
473
+ const provider = providers[id];
276
474
  if (!provider) {
277
- const info = database[id]
278
- if (!info) return
279
- if (info.api && !options["baseURL"]) options["baseURL"] = info.api
475
+ const info = database[id];
476
+ if (!info) return;
477
+ if (info.api && !options['baseURL']) options['baseURL'] = info.api;
280
478
  providers[id] = {
281
479
  source,
282
480
  info,
283
481
  options,
284
482
  getModel,
285
- }
286
- return
483
+ };
484
+ return;
287
485
  }
288
- provider.options = mergeDeep(provider.options, options)
289
- provider.source = source
290
- provider.getModel = getModel ?? provider.getModel
486
+ provider.options = mergeDeep(provider.options, options);
487
+ provider.source = source;
488
+ provider.getModel = getModel ?? provider.getModel;
291
489
  }
292
490
 
293
- const configProviders = Object.entries(config.provider ?? {})
491
+ const configProviders = Object.entries(config.provider ?? {});
294
492
 
295
493
  // 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"] = {
494
+ if (database['github-copilot']) {
495
+ const githubCopilot = database['github-copilot'];
496
+ database['github-copilot-enterprise'] = {
299
497
  ...githubCopilot,
300
- id: "github-copilot-enterprise",
301
- name: "GitHub Copilot Enterprise",
498
+ id: 'github-copilot-enterprise',
499
+ name: 'GitHub Copilot Enterprise',
302
500
  // Enterprise uses a different API endpoint - will be set dynamically based on auth
303
501
  api: undefined,
304
- }
502
+ };
503
+ }
504
+
505
+ // Add Claude OAuth provider that inherits from Anthropic
506
+ // This allows using Claude Code CLI OAuth credentials with the Anthropic API
507
+ if (database['anthropic']) {
508
+ const anthropic = database['anthropic'];
509
+ database['claude-oauth'] = {
510
+ ...anthropic,
511
+ id: 'claude-oauth',
512
+ name: 'Claude OAuth',
513
+ // Use CLAUDE_CODE_OAUTH_TOKEN environment variable
514
+ env: ['CLAUDE_CODE_OAUTH_TOKEN'],
515
+ };
305
516
  }
306
517
 
307
518
  for (const [providerID, provider] of configProviders) {
308
- const existing = database[providerID]
519
+ const existing = database[providerID];
309
520
  const parsed: ModelsDev.Provider = {
310
521
  id: providerID,
311
522
  npm: provider.npm ?? existing?.npm,
@@ -313,15 +524,15 @@ export namespace Provider {
313
524
  env: provider.env ?? existing?.env ?? [],
314
525
  api: provider.api ?? existing?.api,
315
526
  models: existing?.models ?? {},
316
- }
527
+ };
317
528
 
318
529
  for (const [modelID, model] of Object.entries(provider.models ?? {})) {
319
- const existing = parsed.models[model.id ?? modelID]
530
+ const existing = parsed.models[model.id ?? modelID];
320
531
  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
- })
532
+ if (model.name) return model.name;
533
+ if (model.id && model.id !== modelID) return modelID;
534
+ return existing?.name ?? modelID;
535
+ });
325
536
  const parsedModel: ModelsDev.Model = {
326
537
  id: modelID,
327
538
  name,
@@ -355,54 +566,61 @@ export namespace Provider {
355
566
  },
356
567
  modalities: model.modalities ??
357
568
  existing?.modalities ?? {
358
- input: ["text"],
359
- output: ["text"],
569
+ input: ['text'],
570
+ output: ['text'],
360
571
  },
361
572
  headers: model.headers,
362
573
  provider: model.provider ?? existing?.provider,
363
- }
574
+ };
364
575
  if (model.id && model.id !== modelID) {
365
- realIdByKey.set(`${providerID}/${modelID}`, model.id)
576
+ realIdByKey.set(`${providerID}/${modelID}`, model.id);
366
577
  }
367
- parsed.models[modelID] = parsedModel
578
+ parsed.models[modelID] = parsedModel;
368
579
  }
369
- database[providerID] = parsed
580
+ database[providerID] = parsed;
370
581
  }
371
582
 
372
- const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? []))
583
+ const disabled = await Config.get().then(
584
+ (cfg) => new Set(cfg.disabled_providers ?? [])
585
+ );
373
586
  // load env
374
587
  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
588
+ if (disabled.has(providerID)) continue;
589
+ const apiKey = provider.env.map((item) => process.env[item]).at(0);
590
+ if (!apiKey) continue;
378
591
  mergeProvider(
379
592
  providerID,
380
593
  // only include apiKey if there's only one potential option
381
594
  provider.env.length === 1 ? { apiKey } : {},
382
- "env",
383
- )
595
+ 'env'
596
+ );
384
597
  }
385
598
 
386
599
  // load apikeys
387
600
  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")
601
+ if (disabled.has(providerID)) continue;
602
+ if (provider.type === 'api') {
603
+ mergeProvider(providerID, { apiKey: provider.key }, 'api');
391
604
  }
392
605
  }
393
606
 
394
607
  // load custom
395
608
  for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
396
- if (disabled.has(providerID)) continue
397
- const result = await fn(database[providerID])
609
+ if (disabled.has(providerID)) continue;
610
+ const result = await fn(database[providerID]);
398
611
  if (result && (result.autoload || providers[providerID])) {
399
- mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
612
+ mergeProvider(
613
+ providerID,
614
+ result.options ?? {},
615
+ 'custom',
616
+ result.getModel
617
+ );
400
618
  }
401
619
  }
402
620
 
403
621
  // load config
404
622
  for (const [providerID, provider] of configProviders) {
405
- mergeProvider(providerID, provider.options ?? {}, "config")
623
+ mergeProvider(providerID, provider.options ?? {}, 'config');
406
624
  }
407
625
 
408
626
  for (const [providerID, provider] of Object.entries(providers)) {
@@ -411,22 +629,24 @@ export namespace Provider {
411
629
  // Filter out blacklisted models
412
630
  .filter(
413
631
  ([modelID]) =>
414
- modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
632
+ modelID !== 'gpt-5-chat-latest' &&
633
+ !(providerID === 'openrouter' && modelID === 'openai/gpt-5-chat')
415
634
  )
416
635
  // Filter out experimental models
417
636
  .filter(
418
637
  ([, model]) =>
419
- ((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
420
- model.status !== "deprecated",
421
- ),
422
- )
423
- provider.info.models = filteredModels
638
+ ((!model.experimental && model.status !== 'alpha') ||
639
+ Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
640
+ model.status !== 'deprecated'
641
+ )
642
+ );
643
+ provider.info.models = filteredModels;
424
644
 
425
645
  if (Object.keys(provider.info.models).length === 0) {
426
- delete providers[providerID]
427
- continue
646
+ delete providers[providerID];
647
+ continue;
428
648
  }
429
- log.info("found", { providerID })
649
+ log.info('found', { providerID });
430
650
  }
431
651
 
432
652
  return {
@@ -434,34 +654,37 @@ export namespace Provider {
434
654
  providers,
435
655
  sdk,
436
656
  realIdByKey,
437
- }
438
- })
657
+ };
658
+ });
439
659
 
440
660
  export async function list() {
441
- return state().then((state) => state.providers)
661
+ return state().then((state) => state.providers);
442
662
  }
443
663
 
444
664
  async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model) {
445
665
  return (async () => {
446
- using _ = log.time("getSDK", {
666
+ using _ = log.time('getSDK', {
447
667
  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
668
+ });
669
+ const s = await state();
670
+ const pkg = model.provider?.npm ?? provider.npm ?? provider.id;
671
+ const options = { ...s.providers[provider.id]?.options };
672
+ if (
673
+ pkg.includes('@ai-sdk/openai-compatible') &&
674
+ options['includeUsage'] === undefined
675
+ ) {
676
+ options['includeUsage'] = true;
454
677
  }
455
- const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
456
- const existing = s.sdk.get(key)
457
- if (existing) return existing
678
+ const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }));
679
+ const existing = s.sdk.get(key);
680
+ if (existing) return existing;
458
681
 
459
- let installedPath: string
460
- if (!pkg.startsWith("file://")) {
461
- installedPath = await BunProc.install(pkg, "latest")
682
+ let installedPath: string;
683
+ if (!pkg.startsWith('file://')) {
684
+ installedPath = await BunProc.install(pkg, 'latest');
462
685
  } else {
463
- log.info("loading local provider", { pkg })
464
- installedPath = pkg
686
+ log.info('loading local provider', { pkg });
687
+ installedPath = pkg;
465
688
  }
466
689
 
467
690
  // The `google-vertex-anthropic` provider points to the `@ai-sdk/google-vertex` package.
@@ -471,82 +694,86 @@ export namespace Provider {
471
694
  // In addition, Bun's dynamic import logic does not support subpath imports,
472
695
  // so we patch the import path to load directly from `dist`.
473
696
  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) {
697
+ provider.id === 'google-vertex-anthropic'
698
+ ? `${installedPath}/dist/anthropic/index.mjs`
699
+ : installedPath;
700
+ const mod = await import(modPath);
701
+ if (options['timeout'] !== undefined && options['timeout'] !== null) {
477
702
  // 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 ?? {}
703
+ const customFetch = options['fetch'];
704
+ options['fetch'] = async (input: any, init?: BunFetchRequestInit) => {
705
+ const { signal, ...rest } = init ?? {};
481
706
 
482
- const signals: AbortSignal[] = []
483
- if (signal) signals.push(signal)
484
- if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
707
+ const signals: AbortSignal[] = [];
708
+ if (signal) signals.push(signal);
709
+ if (options['timeout'] !== false)
710
+ signals.push(AbortSignal.timeout(options['timeout']));
485
711
 
486
- const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
712
+ const combined =
713
+ signals.length > 1 ? AbortSignal.any(signals) : signals[0];
487
714
 
488
- const fetchFn = customFetch ?? fetch
715
+ const fetchFn = customFetch ?? fetch;
489
716
  return fetchFn(input, {
490
717
  ...rest,
491
718
  signal: combined,
492
719
  // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
493
720
  timeout: false,
494
- })
495
- }
721
+ });
722
+ };
496
723
  }
497
- const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
724
+ const fn = mod[Object.keys(mod).find((key) => key.startsWith('create'))!];
498
725
  const loaded = fn({
499
726
  name: provider.id,
500
727
  ...options,
501
- })
502
- s.sdk.set(key, loaded)
503
- return loaded as SDK
728
+ });
729
+ s.sdk.set(key, loaded);
730
+ return loaded as SDK;
504
731
  })().catch((e) => {
505
- throw new InitError({ providerID: provider.id }, { cause: e })
506
- })
732
+ throw new InitError({ providerID: provider.id }, { cause: e });
733
+ });
507
734
  }
508
735
 
509
736
  export async function getProvider(providerID: string) {
510
- return state().then((s) => s.providers[providerID])
737
+ return state().then((s) => s.providers[providerID]);
511
738
  }
512
739
 
513
740
  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)!
741
+ const key = `${providerID}/${modelID}`;
742
+ const s = await state();
743
+ if (s.models.has(key)) return s.models.get(key)!;
517
744
 
518
- log.info("getModel", {
745
+ log.info('getModel', {
519
746
  providerID,
520
747
  modelID,
521
- })
748
+ });
522
749
 
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)
750
+ const provider = s.providers[providerID];
751
+ if (!provider) throw new ModelNotFoundError({ providerID, modelID });
752
+ const info = provider.info.models[modelID];
753
+ if (!info) throw new ModelNotFoundError({ providerID, modelID });
754
+ const sdk = await getSDK(provider.info, info);
528
755
 
529
756
  try {
530
- const keyReal = `${providerID}/${modelID}`
531
- const realID = s.realIdByKey.get(keyReal) ?? info.id
757
+ const keyReal = `${providerID}/${modelID}`;
758
+ const realID = s.realIdByKey.get(keyReal) ?? info.id;
532
759
  const language = provider.getModel
533
760
  ? await provider.getModel(sdk, realID, provider.options)
534
- : sdk.languageModel(realID)
535
- log.info("found", { providerID, modelID })
761
+ : sdk.languageModel(realID);
762
+ log.info('found', { providerID, modelID });
536
763
  s.models.set(key, {
537
764
  providerID,
538
765
  modelID,
539
766
  info,
540
767
  language,
541
768
  npm: info.provider?.npm ?? provider.info.npm,
542
- })
769
+ });
543
770
  return {
544
771
  modelID,
545
772
  providerID,
546
773
  info,
547
774
  language,
548
775
  npm: info.provider?.npm ?? provider.info.npm,
549
- }
776
+ };
550
777
  } catch (e) {
551
778
  if (e instanceof NoSuchModelError)
552
779
  throw new ModelNotFoundError(
@@ -554,83 +781,97 @@ export namespace Provider {
554
781
  modelID: modelID,
555
782
  providerID,
556
783
  },
557
- { cause: e },
558
- )
559
- throw e
784
+ { cause: e }
785
+ );
786
+ throw e;
560
787
  }
561
788
  }
562
789
 
563
790
  export async function getSmallModel(providerID: string) {
564
- const cfg = await Config.get()
791
+ const cfg = await Config.get();
565
792
 
566
793
  if (cfg.small_model) {
567
- const parsed = parseModel(cfg.small_model)
568
- return getModel(parsed.providerID, parsed.modelID)
794
+ const parsed = parseModel(cfg.small_model);
795
+ return getModel(parsed.providerID, parsed.modelID);
569
796
  }
570
797
 
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"]
798
+ const provider = await state().then((state) => state.providers[providerID]);
799
+ if (!provider) return;
800
+ let priority = [
801
+ 'claude-haiku-4-5',
802
+ 'claude-haiku-4.5',
803
+ '3-5-haiku',
804
+ '3.5-haiku',
805
+ 'gemini-2.5-flash',
806
+ 'gpt-5-nano',
807
+ ];
574
808
  // 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")
809
+ if (providerID === 'github-copilot') {
810
+ priority = priority.filter((m) => m !== 'claude-haiku-4.5');
577
811
  }
578
- if (providerID === "opencode" || providerID === "local") {
579
- priority = ["gpt-5-nano"]
812
+ if (providerID === 'opencode' || providerID === 'local') {
813
+ priority = ['gpt-5-nano'];
580
814
  }
581
815
  for (const item of priority) {
582
816
  for (const model of Object.keys(provider.info.models)) {
583
- if (model.includes(item)) return getModel(providerID, model)
817
+ if (model.includes(item)) return getModel(providerID, model);
584
818
  }
585
819
  }
586
820
  }
587
821
 
588
- const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
822
+ const priority = ['gpt-5', 'claude-sonnet-4', 'big-pickle', 'gemini-3-pro'];
589
823
  export function sort(models: ModelsDev.Model[]) {
590
824
  return sortBy(
591
825
  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
- )
826
+ [
827
+ (model) => priority.findIndex((filter) => model.id.includes(filter)),
828
+ 'desc',
829
+ ],
830
+ [(model) => (model.id.includes('latest') ? 0 : 1), 'asc'],
831
+ [(model) => model.id, 'desc']
832
+ );
596
833
  }
597
834
 
598
835
  export async function defaultModel() {
599
- const cfg = await Config.get()
600
- if (cfg.model) return parseModel(cfg.model)
836
+ const cfg = await Config.get();
837
+ if (cfg.model) return parseModel(cfg.model);
601
838
 
602
839
  const provider = await list()
603
840
  .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")
841
+ .then((x) =>
842
+ x.find(
843
+ (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)
844
+ )
845
+ );
846
+ if (!provider) throw new Error('no providers found');
847
+ const [model] = sort(Object.values(provider.info.models));
848
+ if (!model) throw new Error('no models found');
608
849
  return {
609
850
  providerID: provider.info.id,
610
851
  modelID: model.id,
611
- }
852
+ };
612
853
  }
613
854
 
614
855
  export function parseModel(model: string) {
615
- const [providerID, ...rest] = model.split("/")
856
+ const [providerID, ...rest] = model.split('/');
616
857
  return {
617
858
  providerID: providerID,
618
- modelID: rest.join("/"),
619
- }
859
+ modelID: rest.join('/'),
860
+ };
620
861
  }
621
862
 
622
863
  export const ModelNotFoundError = NamedError.create(
623
- "ProviderModelNotFoundError",
864
+ 'ProviderModelNotFoundError',
624
865
  z.object({
625
866
  providerID: z.string(),
626
867
  modelID: z.string(),
627
- }),
628
- )
868
+ })
869
+ );
629
870
 
630
871
  export const InitError = NamedError.create(
631
- "ProviderInitError",
872
+ 'ProviderInitError',
632
873
  z.object({
633
874
  providerID: z.string(),
634
- }),
635
- )
875
+ })
876
+ );
636
877
  }