openllmprovider 0.1.0
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/README.md +192 -0
- package/dist/auth/index.cjs +6 -0
- package/dist/auth/index.d.cts +3 -0
- package/dist/auth/index.d.mts +3 -0
- package/dist/auth/index.mjs +3 -0
- package/dist/auto-C2hXJY13.d.cts +33 -0
- package/dist/auto-C2hXJY13.d.cts.map +1 -0
- package/dist/auto-CBqNYBXs.mjs +48 -0
- package/dist/auto-CBqNYBXs.mjs.map +1 -0
- package/dist/auto-CInerwvs.d.mts +33 -0
- package/dist/auto-CInerwvs.d.mts.map +1 -0
- package/dist/auto-D77wgMqO.cjs +59 -0
- package/dist/auto-D77wgMqO.cjs.map +1 -0
- package/dist/file-DB-rxfzi.mjs +77 -0
- package/dist/file-DB-rxfzi.mjs.map +1 -0
- package/dist/file-DZ7FGcSW.cjs +73 -0
- package/dist/file-DZ7FGcSW.cjs.map +1 -0
- package/dist/index.cjs +1909 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1239 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +1241 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1891 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger-BsHpI_fH.mjs +11 -0
- package/dist/logger-BsHpI_fH.mjs.map +1 -0
- package/dist/logger-jRimlMFR.cjs +69 -0
- package/dist/logger-jRimlMFR.cjs.map +1 -0
- package/dist/plugin/index.cjs +29 -0
- package/dist/plugin/index.cjs.map +1 -0
- package/dist/plugin/index.d.cts +10 -0
- package/dist/plugin/index.d.cts.map +1 -0
- package/dist/plugin/index.d.mts +10 -0
- package/dist/plugin/index.d.mts.map +1 -0
- package/dist/plugin/index.mjs +25 -0
- package/dist/plugin/index.mjs.map +1 -0
- package/dist/plugin-BkeUu5LW.d.mts +46 -0
- package/dist/plugin-BkeUu5LW.d.mts.map +1 -0
- package/dist/plugin-wK7RmJhZ.d.cts +46 -0
- package/dist/plugin-wK7RmJhZ.d.cts.map +1 -0
- package/dist/resolver-BA7LWSJO.mjs +645 -0
- package/dist/resolver-BA7LWSJO.mjs.map +1 -0
- package/dist/resolver-BMTvzTt9.cjs +662 -0
- package/dist/resolver-BMTvzTt9.cjs.map +1 -0
- package/dist/resolver-MgJryMWG.d.cts +75 -0
- package/dist/resolver-MgJryMWG.d.cts.map +1 -0
- package/dist/resolver-_gfXzr_S.d.mts +76 -0
- package/dist/resolver-_gfXzr_S.d.mts.map +1 -0
- package/dist/storage/index.cjs +7 -0
- package/dist/storage/index.d.cts +12 -0
- package/dist/storage/index.d.cts.map +1 -0
- package/dist/storage/index.d.mts +12 -0
- package/dist/storage/index.d.mts.map +1 -0
- package/dist/storage/index.mjs +4 -0
- package/package.json +137 -0
- package/src/auth/.gitkeep +0 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/resolver.ts +46 -0
- package/src/auth/scanners.ts +462 -0
- package/src/auth/store.ts +357 -0
- package/src/catalog/.gitkeep +0 -0
- package/src/catalog/catalog.ts +302 -0
- package/src/catalog/index.ts +17 -0
- package/src/catalog/mapper.ts +129 -0
- package/src/catalog/merger.ts +99 -0
- package/src/index.ts +37 -0
- package/src/logger.ts +7 -0
- package/src/plugin/.gitkeep +0 -0
- package/src/plugin/anthropic.test.ts +505 -0
- package/src/plugin/anthropic.ts +324 -0
- package/src/plugin/codex.ts +656 -0
- package/src/plugin/copilot.ts +161 -0
- package/src/plugin/google.ts +454 -0
- package/src/plugin/index.ts +30 -0
- package/src/provider/.gitkeep +0 -0
- package/src/provider/bundled.ts +59 -0
- package/src/provider/index.ts +249 -0
- package/src/provider/state.ts +163 -0
- package/src/storage/.gitkeep +0 -0
- package/src/storage/auto.ts +32 -0
- package/src/storage/file.ts +84 -0
- package/src/storage/index.ts +10 -0
- package/src/storage/memory.ts +23 -0
- package/src/types/.gitkeep +0 -0
- package/src/types/auth.ts +18 -0
- package/src/types/errors.ts +87 -0
- package/src/types/index.ts +26 -0
- package/src/types/model.ts +88 -0
- package/src/types/plugin.ts +49 -0
- package/src/types/provider.ts +48 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
4
|
+
import { createInterface } from 'node:readline/promises'
|
|
5
|
+
import { createLogger } from '../logger.js'
|
|
6
|
+
import type { AuthCredential, AuthHook } from '../types/plugin.js'
|
|
7
|
+
|
|
8
|
+
const log = createLogger('plugin:anthropic')
|
|
9
|
+
|
|
10
|
+
const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e'
|
|
11
|
+
const AUTHORIZE_URL = 'https://claude.ai/oauth/authorize'
|
|
12
|
+
const TOKEN_URL = 'https://console.anthropic.com/v1/oauth/token'
|
|
13
|
+
const API_KEY_EXCHANGE_URL = 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key'
|
|
14
|
+
const REDIRECT_URI = 'https://console.anthropic.com/oauth/code/callback'
|
|
15
|
+
const OAUTH_SCOPES = 'org:create_api_key user:profile user:inference'
|
|
16
|
+
const OAUTH_BETA = 'oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14'
|
|
17
|
+
|
|
18
|
+
interface TokenResponse {
|
|
19
|
+
access_token: string
|
|
20
|
+
refresh_token?: string
|
|
21
|
+
expires_in?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CreateApiKeyResponse {
|
|
25
|
+
raw_key?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isOAuthToken(value: string): boolean {
|
|
29
|
+
return value.startsWith('sk-ant-oat') || value.startsWith('sk-ant-ort')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function looksLikeApiKey(value: string): boolean {
|
|
33
|
+
if (isOAuthToken(value)) return false
|
|
34
|
+
return value.startsWith('sk-ant-')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function applyBearerHeaders(headers: Headers, token: string): void {
|
|
38
|
+
headers.delete('x-api-key')
|
|
39
|
+
headers.delete('authorization')
|
|
40
|
+
headers.delete('Authorization')
|
|
41
|
+
headers.set('Authorization', `Bearer ${token}`)
|
|
42
|
+
headers.set('anthropic-beta', OAUTH_BETA)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toBase64Url(inputValue: Buffer): string {
|
|
46
|
+
return inputValue.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createPkce(): { verifier: string; challenge: string } {
|
|
50
|
+
const verifier = toBase64Url(randomBytes(32))
|
|
51
|
+
const challenge = toBase64Url(createHash('sha256').update(verifier).digest())
|
|
52
|
+
return { verifier, challenge }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildAuthorizationRequest(): { url: string; verifier: string; state: string } {
|
|
56
|
+
const { verifier, challenge } = createPkce()
|
|
57
|
+
const state = verifier
|
|
58
|
+
const url = new URL(AUTHORIZE_URL)
|
|
59
|
+
url.searchParams.set('code', 'true')
|
|
60
|
+
url.searchParams.set('client_id', CLIENT_ID)
|
|
61
|
+
url.searchParams.set('response_type', 'code')
|
|
62
|
+
url.searchParams.set('redirect_uri', REDIRECT_URI)
|
|
63
|
+
url.searchParams.set('scope', OAUTH_SCOPES)
|
|
64
|
+
url.searchParams.set('code_challenge', challenge)
|
|
65
|
+
url.searchParams.set('code_challenge_method', 'S256')
|
|
66
|
+
url.searchParams.set('state', state)
|
|
67
|
+
return { url: url.toString(), verifier, state }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function openUrlInBrowser(url: string): void {
|
|
71
|
+
const platform = process.platform
|
|
72
|
+
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'rundll32' : 'xdg-open'
|
|
73
|
+
const args = platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url]
|
|
74
|
+
try {
|
|
75
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' })
|
|
76
|
+
child.unref()
|
|
77
|
+
} catch {
|
|
78
|
+
log('failed to open browser automatically')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function readCallbackInput(question: string): Promise<string> {
|
|
83
|
+
const rl = createInterface({ input, output })
|
|
84
|
+
try {
|
|
85
|
+
return (await rl.question(question)).trim()
|
|
86
|
+
} finally {
|
|
87
|
+
rl.close()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseCallbackInput(value: string): { code?: string; state?: string } {
|
|
92
|
+
const raw = value.trim()
|
|
93
|
+
if (raw.length === 0) return {}
|
|
94
|
+
|
|
95
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
96
|
+
try {
|
|
97
|
+
const u = new URL(raw)
|
|
98
|
+
return {
|
|
99
|
+
code: u.searchParams.get('code') ?? undefined,
|
|
100
|
+
state: u.searchParams.get('state') ?? undefined,
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
return {}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const maybeQuery = raw.startsWith('?') ? raw.slice(1) : raw
|
|
108
|
+
if (maybeQuery.includes('=')) {
|
|
109
|
+
const params = new URLSearchParams(maybeQuery)
|
|
110
|
+
const code = params.get('code') ?? undefined
|
|
111
|
+
const state = params.get('state') ?? undefined
|
|
112
|
+
if (code !== undefined || state !== undefined) return { code, state }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { code: raw }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function exchangeAuthorizationCode(code: string, verifier: string, state: string): Promise<TokenResponse> {
|
|
119
|
+
const res = await globalThis.fetch(TOKEN_URL, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
Accept: 'application/json',
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
grant_type: 'authorization_code',
|
|
127
|
+
code,
|
|
128
|
+
state,
|
|
129
|
+
client_id: CLIENT_ID,
|
|
130
|
+
redirect_uri: REDIRECT_URI,
|
|
131
|
+
code_verifier: verifier,
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
const body = await res.text()
|
|
137
|
+
throw new Error(`Anthropic token exchange failed: ${res.status} ${res.statusText} ${body}`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const raw = (await res.json()) as Record<string, unknown>
|
|
141
|
+
return {
|
|
142
|
+
access_token: String(raw.access_token ?? ''),
|
|
143
|
+
refresh_token: typeof raw.refresh_token === 'string' ? raw.refresh_token : undefined,
|
|
144
|
+
expires_in: typeof raw.expires_in === 'number' ? raw.expires_in : undefined,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
|
|
149
|
+
const res = await globalThis.fetch(TOKEN_URL, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
Accept: 'application/json',
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
grant_type: 'refresh_token',
|
|
157
|
+
refresh_token: refreshToken,
|
|
158
|
+
client_id: CLIENT_ID,
|
|
159
|
+
}),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
const body = await res.text().catch(() => '')
|
|
164
|
+
throw new Error(`Anthropic token refresh failed: ${res.status} ${res.statusText} ${body}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const raw = (await res.json()) as Record<string, unknown>
|
|
168
|
+
return {
|
|
169
|
+
access_token: String(raw.access_token ?? ''),
|
|
170
|
+
refresh_token: typeof raw.refresh_token === 'string' ? raw.refresh_token : undefined,
|
|
171
|
+
expires_in: typeof raw.expires_in === 'number' ? raw.expires_in : undefined,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function createApiKeyFromOAuthAccessToken(accessToken: string): Promise<string> {
|
|
176
|
+
const res = await globalThis.fetch(API_KEY_EXCHANGE_URL, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: {
|
|
179
|
+
Authorization: `Bearer ${accessToken}`,
|
|
180
|
+
'Content-Type': 'application/json',
|
|
181
|
+
Accept: 'application/json',
|
|
182
|
+
},
|
|
183
|
+
body: '{}',
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
const body = await res.text()
|
|
188
|
+
throw new Error(`Anthropic API key exchange failed: ${res.status} ${res.statusText} ${body}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const raw = (await res.json()) as CreateApiKeyResponse
|
|
192
|
+
if (typeof raw.raw_key !== 'string' || raw.raw_key.length === 0) {
|
|
193
|
+
throw new Error('Anthropic API key exchange returned empty raw_key')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return raw.raw_key
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolve a fresh OAuth access_token from the credential, refreshing if expired.
|
|
201
|
+
* Used both during loader setup and inside the per-request Bearer fallback fetch.
|
|
202
|
+
* When a refresh occurs and setAuth is provided, the updated credential is persisted.
|
|
203
|
+
*/
|
|
204
|
+
async function resolveOAuthToken(
|
|
205
|
+
auth: AuthCredential,
|
|
206
|
+
setAuth?: (credential: AuthCredential) => Promise<void>,
|
|
207
|
+
): Promise<string | undefined> {
|
|
208
|
+
let token = auth.key
|
|
209
|
+
if (auth.expires !== undefined && auth.expires < Date.now() && typeof auth.refresh === 'string') {
|
|
210
|
+
try {
|
|
211
|
+
const refreshed = await refreshAccessToken(auth.refresh)
|
|
212
|
+
token = refreshed.access_token
|
|
213
|
+
if (setAuth) {
|
|
214
|
+
const updated: AuthCredential = {
|
|
215
|
+
...auth,
|
|
216
|
+
key: refreshed.access_token,
|
|
217
|
+
refresh: refreshed.refresh_token ?? auth.refresh,
|
|
218
|
+
expires: refreshed.expires_in !== undefined ? Date.now() + refreshed.expires_in * 1000 : undefined,
|
|
219
|
+
}
|
|
220
|
+
await setAuth(updated).catch((err) =>
|
|
221
|
+
log('failed to persist refreshed credential: %s', err instanceof Error ? err.message : String(err)),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
log('anthropic token refresh failed: %s', error instanceof Error ? error.message : String(error))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return typeof token === 'string' && token.length > 0 ? token : undefined
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Anthropic auth supports two modes:
|
|
233
|
+
//
|
|
234
|
+
// 1. API key (type: 'api')
|
|
235
|
+
// - key holds a direct API key (sk-ant-api03-xxx)
|
|
236
|
+
// - The loader returns {} — no custom config needed, the SDK uses x-api-key
|
|
237
|
+
// header automatically.
|
|
238
|
+
//
|
|
239
|
+
// 2. OAuth (type: 'oauth')
|
|
240
|
+
// - key holds the short-lived access_token (sk-ant-oat-xxx)
|
|
241
|
+
// - refresh holds the long-lived refresh_token (sk-ant-ort-xxx)
|
|
242
|
+
// - expires is the absolute timestamp (ms) when the access_token expires
|
|
243
|
+
// - The loader handles the full lifecycle:
|
|
244
|
+
// a. If the access_token is expired and a refresh_token exists, refresh it
|
|
245
|
+
// b. If the token looks like an API key (sk-ant- but not oat/ort), use as apiKey
|
|
246
|
+
// c. Otherwise, exchange the OAuth access_token for an API key via
|
|
247
|
+
// /api/oauth/claude_cli/create_api_key
|
|
248
|
+
// d. If exchange fails, fall back to Bearer token auth with custom fetch
|
|
249
|
+
//
|
|
250
|
+
// IMPORTANT: Anthropic uses refresh token rotation — each refresh request
|
|
251
|
+
// invalidates the old refresh_token and returns a new one. This means OAuth
|
|
252
|
+
// credentials CANNOT be shared across applications. If opencode and this
|
|
253
|
+
// project both hold the same credential, whichever refreshes first will
|
|
254
|
+
// invalidate the other's refresh_token, causing a 400 error on subsequent
|
|
255
|
+
// refresh attempts. Each application must perform its own OAuth flow and
|
|
256
|
+
// maintain its own credential independently.
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
export const anthropicPlugin: AuthHook = {
|
|
259
|
+
provider: 'anthropic',
|
|
260
|
+
|
|
261
|
+
// API key auth: loader is a no-op — the SDK handles x-api-key header directly
|
|
262
|
+
async loader(getAuth, _provider, setAuth) {
|
|
263
|
+
const auth = await getAuth()
|
|
264
|
+
if (auth.type !== 'oauth') return {}
|
|
265
|
+
|
|
266
|
+
// OAuth: resolve a fresh token (refresh if expired), then try apiKey paths
|
|
267
|
+
const token = await resolveOAuthToken(auth, setAuth)
|
|
268
|
+
|
|
269
|
+
if (token !== undefined) {
|
|
270
|
+
if (looksLikeApiKey(token)) {
|
|
271
|
+
return { apiKey: token }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const apiKey = await createApiKeyFromOAuthAccessToken(token)
|
|
276
|
+
return { apiKey }
|
|
277
|
+
} catch (error) {
|
|
278
|
+
log('anthropic api key exchange failed, using oauth bearer fallback: %s', String(error))
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Bearer fallback: per-request token refresh (same pattern as google/codex plugins)
|
|
283
|
+
return {
|
|
284
|
+
headers: {
|
|
285
|
+
'anthropic-beta': OAUTH_BETA,
|
|
286
|
+
},
|
|
287
|
+
async fetch(request: Parameters<typeof globalThis.fetch>[0], init?: Parameters<typeof globalThis.fetch>[1]) {
|
|
288
|
+
const currentAuth = await getAuth()
|
|
289
|
+
const bearerToken = await resolveOAuthToken(currentAuth, setAuth)
|
|
290
|
+
const headers = new Headers(init?.headers)
|
|
291
|
+
applyBearerHeaders(headers, bearerToken ?? '')
|
|
292
|
+
return globalThis.fetch(request, { ...init, headers })
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
methods: [
|
|
298
|
+
{
|
|
299
|
+
type: 'oauth',
|
|
300
|
+
label: 'Claude Pro/Max (Browser OAuth)',
|
|
301
|
+
async handler(): Promise<AuthCredential> {
|
|
302
|
+
const authRequest = buildAuthorizationRequest()
|
|
303
|
+
console.log('Open this URL to continue Claude Pro/Max OAuth:')
|
|
304
|
+
console.log(authRequest.url)
|
|
305
|
+
openUrlInBrowser(authRequest.url)
|
|
306
|
+
|
|
307
|
+
const callbackInput = await readCallbackInput('Paste the callback URL or authorization code: ')
|
|
308
|
+
const parsed = parseCallbackInput(callbackInput)
|
|
309
|
+
if (!parsed.code) throw new Error('Missing authorization code in callback input')
|
|
310
|
+
if (parsed.state !== undefined && parsed.state !== authRequest.state) {
|
|
311
|
+
throw new Error('OAuth state mismatch')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const tokens = await exchangeAuthorizationCode(parsed.code, authRequest.verifier, authRequest.state)
|
|
315
|
+
return {
|
|
316
|
+
type: 'oauth',
|
|
317
|
+
key: tokens.access_token,
|
|
318
|
+
refresh: tokens.refresh_token,
|
|
319
|
+
expires: tokens.expires_in !== undefined ? Date.now() + tokens.expires_in * 1000 : undefined,
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
}
|