opencode-translate 0.1.2 → 0.2.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.
Files changed (41) hide show
  1. package/README.md +11 -11
  2. package/package.json +7 -4
  3. package/src/activation/chat-message.ts +164 -0
  4. package/src/activation/index.ts +38 -0
  5. package/src/activation/logging.ts +11 -0
  6. package/src/activation/messages-transform.ts +38 -0
  7. package/src/activation/metadata.ts +41 -0
  8. package/src/activation/parts.ts +53 -0
  9. package/src/activation/question-hooks.ts +95 -0
  10. package/src/activation/state.ts +97 -0
  11. package/src/activation/text-complete.ts +51 -0
  12. package/src/activation/trigger.ts +57 -0
  13. package/src/activation/types.ts +41 -0
  14. package/src/activation.ts +1 -633
  15. package/src/anthropic-oauth.ts +3 -3
  16. package/src/auth/codex-request.ts +108 -0
  17. package/src/auth/codex-response.ts +78 -0
  18. package/src/auth/codex-shared.ts +3 -0
  19. package/src/auth/headers.ts +18 -0
  20. package/src/auth/index.ts +153 -0
  21. package/src/auth/oauth-fetch.ts +100 -0
  22. package/src/auth/refresh.ts +102 -0
  23. package/src/auth/retry.ts +70 -0
  24. package/src/auth/store.ts +45 -0
  25. package/src/auth/types.ts +27 -0
  26. package/src/auth.ts +1 -725
  27. package/src/constants/errors.ts +24 -0
  28. package/src/constants/guards.ts +33 -0
  29. package/src/constants/options.ts +41 -0
  30. package/src/constants/plugin.ts +10 -0
  31. package/src/constants/types.ts +144 -0
  32. package/src/constants.ts +5 -261
  33. package/src/labels.ts +2 -16
  34. package/src/prompts.ts +2 -17
  35. package/src/question-tool.ts +1 -1
  36. package/src/translator/index.ts +125 -0
  37. package/src/translator/part-id.ts +43 -0
  38. package/src/translator/provider.ts +81 -0
  39. package/src/translator/retry.ts +62 -0
  40. package/src/translator/types.ts +17 -0
  41. package/src/translator.ts +1 -326
package/src/auth.ts CHANGED
@@ -1,725 +1 @@
1
- import { readFile, stat } from "node:fs/promises"
2
- import os from "node:os"
3
- import path from "node:path"
4
- import { setTimeout as sleep } from "node:timers/promises"
5
- import {
6
- isAnthropicMessagesRequest,
7
- rewriteMessagesBody,
8
- rewriteMessagesURL,
9
- setOAuthHeaders as setAnthropicOAuthHeaders,
10
- } from "./anthropic-oauth"
11
- import {
12
- AUTH_ENV_FALLBACK,
13
- type AuthInfo,
14
- buildAuthUnavailableError,
15
- buildOAuthRefreshError,
16
- type FetchLike,
17
- getEnvVarHint,
18
- normalizeReason,
19
- OAUTH_DUMMY_KEY,
20
- type OAuthInfo,
21
- type PluginClientLike,
22
- type ProviderInfo,
23
- parseTranslatorModel,
24
- type ResolvedTranslateOptions,
25
- USER_AGENT,
26
- unwrapData,
27
- } from "./constants"
28
-
29
- export interface ResolvedCredential {
30
- providerID: string
31
- provider?: ProviderInfo
32
- apiKey?: string
33
- fetch?: FetchLike
34
- mode: "apiKey" | "oauth" | "default"
35
- }
36
-
37
- interface AuthDependencies {
38
- fetchImpl?: FetchLike
39
- now?: () => number
40
- sleep?: (ms: number) => Promise<void>
41
- readFile?: (filePath: string, encoding: BufferEncoding) => Promise<string>
42
- stat?: (filePath: string) => Promise<{ mode: number }>
43
- packageVersion?: string
44
- }
45
-
46
- const credentialCache = new Map<string, ResolvedCredential>()
47
- const oauthRefreshInflight = new Map<string, Promise<OAuthInfo>>()
48
-
49
- export function __resetAuthCachesForTest() {
50
- credentialCache.clear()
51
- oauthRefreshInflight.clear()
52
- }
53
-
54
- // opencode itself resolves `auth.json` through xdg-basedir (packages/opencode/src/global/index.ts:10).
55
- // When XDG_DATA_HOME is unset, xdg-basedir returns `~/.local/share` on macOS and Linux, and
56
- // %LOCALAPPDATA% on Windows. The spec's macOS fallback to `~/Library/Application Support` is only
57
- // accurate if the user exports XDG_DATA_HOME themselves; in practice opencode stores auth.json at
58
- // `~/.local/share/opencode/auth.json` on macOS, so we mirror that here.
59
- function authFilePath(): string {
60
- const xdgDataHome = process.env.XDG_DATA_HOME
61
- if (xdgDataHome) return path.join(xdgDataHome, "opencode", "auth.json")
62
- if (process.platform === "win32") {
63
- return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "opencode", "auth.json")
64
- }
65
- return path.join(os.homedir(), ".local", "share", "opencode", "auth.json")
66
- }
67
-
68
- function copyHeaders(headers?: HeadersInit): Headers {
69
- return new Headers(headers)
70
- }
71
-
72
- function headerValue(headers: Headers, key: string): string | undefined {
73
- const value = headers.get(key)
74
- return value === null ? undefined : value
75
- }
76
-
77
- function setUserAgent(headers: Headers, packageVersion?: string) {
78
- headers.set("User-Agent", packageVersion ? `${USER_AGENT.replace("0.0.0", packageVersion)}` : USER_AGENT)
79
- }
80
-
81
- function isRecord(value: unknown): value is Record<string, unknown> {
82
- return !!value && typeof value === "object" && !Array.isArray(value)
83
- }
84
-
85
- function textFromContent(content: unknown): string | undefined {
86
- if (typeof content === "string") return content
87
- if (!Array.isArray(content)) return undefined
88
-
89
- const text = content
90
- .map((part) => (isRecord(part) && typeof part.text === "string" ? part.text : undefined))
91
- .filter((value): value is string => value !== undefined)
92
- .join("\n")
93
-
94
- return text || undefined
95
- }
96
-
97
- function normalizeCodexContent(role: string, content: unknown): Record<string, unknown>[] {
98
- const textType = role === "assistant" ? "output_text" : "input_text"
99
- if (typeof content === "string") return [{ type: textType, text: content }]
100
- if (!Array.isArray(content)) return []
101
-
102
- const result: Record<string, unknown>[] = []
103
- for (const part of content) {
104
- if (!isRecord(part)) continue
105
- const type = part.type
106
- if (type === "input_text" || type === "output_text") {
107
- result.push({ ...part, type: textType })
108
- continue
109
- }
110
- if (type === "input_image") {
111
- result.push({ ...part })
112
- continue
113
- }
114
- if (typeof part.text === "string") {
115
- result.push({ type: textType, text: part.text })
116
- }
117
- }
118
-
119
- return result
120
- }
121
-
122
- function normalizeCodexInputItem(item: unknown, instructions: string[]): unknown | undefined {
123
- if (!isRecord(item)) return item
124
- const role = typeof item.role === "string" ? item.role : undefined
125
-
126
- if (role === "system" || role === "developer") {
127
- const text = textFromContent(item.content)
128
- if (text) instructions.push(text)
129
- return undefined
130
- }
131
-
132
- if (item.type === "message" && role) {
133
- const content = normalizeCodexContent(role, item.content)
134
- return content.length > 0 ? { ...item, role, content } : undefined
135
- }
136
-
137
- if (role) {
138
- const content = normalizeCodexContent(role, item.content)
139
- return content.length > 0 ? { type: "message", role, content } : undefined
140
- }
141
-
142
- return item
143
- }
144
-
145
- interface CodexBodyRewrite {
146
- body: BodyInit | null | undefined
147
- originalStream: boolean
148
- }
149
-
150
- function rewriteOpenAICodexBody(body: BodyInit | null | undefined): CodexBodyRewrite {
151
- if (typeof body !== "string") return { body, originalStream: false }
152
-
153
- let parsed: unknown
154
- try {
155
- parsed = JSON.parse(body)
156
- } catch {
157
- return { body, originalStream: false }
158
- }
159
-
160
- if (!isRecord(parsed)) return { body, originalStream: false }
161
- const originalStream = parsed.stream === true
162
-
163
- const sourceInput = Array.isArray(parsed.input)
164
- ? parsed.input
165
- : Array.isArray(parsed.messages)
166
- ? parsed.messages
167
- : undefined
168
- if (!sourceInput) return { body, originalStream }
169
-
170
- const instructions: string[] = []
171
- if (typeof parsed.instructions === "string" && parsed.instructions) instructions.push(parsed.instructions)
172
-
173
- const input = sourceInput
174
- .map((item) => normalizeCodexInputItem(item, instructions))
175
- .filter((item): item is unknown => item !== undefined)
176
-
177
- const include = Array.isArray(parsed.include)
178
- ? parsed.include.filter((item): item is string => typeof item === "string")
179
- : []
180
- if (!include.includes("reasoning.encrypted_content")) include.push("reasoning.encrypted_content")
181
-
182
- return {
183
- body: JSON.stringify({
184
- ...parsed,
185
- instructions: instructions.join("\n\n"),
186
- input,
187
- tools: Array.isArray(parsed.tools) ? parsed.tools : [],
188
- tool_choice: typeof parsed.tool_choice === "string" ? parsed.tool_choice : "auto",
189
- parallel_tool_calls: typeof parsed.parallel_tool_calls === "boolean" ? parsed.parallel_tool_calls : false,
190
- store: false,
191
- stream: true,
192
- include,
193
- max_output_tokens: undefined,
194
- max_completion_tokens: undefined,
195
- messages: undefined,
196
- }),
197
- originalStream,
198
- }
199
- }
200
-
201
- function normalizeCodexOutputItem(item: unknown, index: number): unknown | undefined {
202
- if (!isRecord(item)) return undefined
203
- if (item.type !== "message" || item.role !== "assistant") return item
204
- if (!Array.isArray(item.content)) return undefined
205
-
206
- const content: Record<string, unknown>[] = []
207
- for (const part of item.content) {
208
- if (!isRecord(part) || part.type !== "output_text" || typeof part.text !== "string") continue
209
- content.push({ ...part, annotations: Array.isArray(part.annotations) ? part.annotations : [] })
210
- }
211
-
212
- if (content.length === 0) return undefined
213
- return {
214
- ...item,
215
- id: typeof item.id === "string" ? item.id : `msg_opencode_translate_${index}`,
216
- role: "assistant",
217
- content,
218
- }
219
- }
220
-
221
- function buildCodexTextOutput(text: string): Record<string, unknown> {
222
- return {
223
- type: "message",
224
- id: "msg_opencode_translate_0",
225
- role: "assistant",
226
- content: [{ type: "output_text", text, annotations: [] }],
227
- }
228
- }
229
-
230
- function parseCodexSSEResponse(text: string): unknown | undefined {
231
- let finalResponse: unknown
232
- let deltaText = ""
233
- const outputItems: unknown[] = []
234
-
235
- for (const line of text.split(/\r?\n/)) {
236
- if (!line.startsWith("data: ")) continue
237
- const payload = line.slice(6).trim()
238
- if (!payload || payload === "[DONE]") continue
239
- try {
240
- const parsed = JSON.parse(payload) as Record<string, unknown>
241
- if (parsed.type === "response.output_text.delta" && typeof parsed.delta === "string") {
242
- deltaText += parsed.delta
243
- continue
244
- }
245
- if (
246
- (parsed.type === "response.output_item.done" || parsed.type === "response.output_item.added") &&
247
- parsed.item
248
- ) {
249
- outputItems.push(parsed.item)
250
- continue
251
- }
252
- if ((parsed.type === "response.done" || parsed.type === "response.completed") && parsed.response) {
253
- finalResponse = parsed.response
254
- }
255
- } catch {}
256
- }
257
-
258
- if (!finalResponse && !deltaText && outputItems.length === 0) return undefined
259
-
260
- const response: Record<string, unknown> = isRecord(finalResponse)
261
- ? { ...finalResponse }
262
- : { id: "resp_opencode_translate" }
263
- const existingOutput: unknown[] = Array.isArray(response.output) ? response.output : []
264
- const sourceOutput = existingOutput.length > 0 ? existingOutput : outputItems
265
- const normalizedOutput = sourceOutput
266
- .map((item, index) => normalizeCodexOutputItem(item, index))
267
- .filter((item): item is unknown => item !== undefined)
268
-
269
- response.output = normalizedOutput.length > 0 ? normalizedOutput : deltaText ? [buildCodexTextOutput(deltaText)] : []
270
- return response
271
- }
272
-
273
- async function convertCodexSSEToJSON(response: Response): Promise<Response> {
274
- const headers = new Headers(response.headers)
275
- const text = await response.text()
276
- const parsed = parseCodexSSEResponse(text)
277
- if (!parsed) return new Response(text, { status: response.status, statusText: response.statusText, headers })
278
-
279
- headers.set("content-type", "application/json; charset=utf-8")
280
- return new Response(JSON.stringify(parsed), { status: response.status, statusText: response.statusText, headers })
281
- }
282
-
283
- function isMissingCredentialError(error: unknown): boolean {
284
- const message = normalizeReason(error).toLowerCase()
285
- return (
286
- message.includes("api key") ||
287
- message.includes("api-key") ||
288
- message.includes("missing credentials") ||
289
- message.includes("missing authentication") ||
290
- message.includes("missing auth") ||
291
- message.includes("no auth")
292
- )
293
- }
294
-
295
- function getStatus(error: unknown): number | undefined {
296
- if (!error || typeof error !== "object") return undefined
297
- const record = error as Record<string, unknown>
298
- if (typeof record.status === "number") return record.status
299
- if (typeof record.statusCode === "number") return record.statusCode
300
- const response = record.response
301
- if (response && typeof response === "object") {
302
- const maybeStatus = (response as Record<string, unknown>).status
303
- if (typeof maybeStatus === "number") return maybeStatus
304
- }
305
- return undefined
306
- }
307
-
308
- function getRetryAfterMs(error: unknown): number {
309
- if (!error || typeof error !== "object") return 2000
310
- const record = error as Record<string, unknown>
311
- const response = record.response
312
- if (response && typeof response === "object") {
313
- const headers = (response as { headers?: Headers }).headers
314
- if (headers instanceof Headers) {
315
- const retryAfter = headerValue(headers, "retry-after")
316
- if (!retryAfter) return 2000
317
- const seconds = Number(retryAfter)
318
- if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000)
319
- const date = Date.parse(retryAfter)
320
- if (Number.isFinite(date)) return Math.max(0, date - Date.now())
321
- }
322
- }
323
- return 2000
324
- }
325
-
326
- function isRetryableError(error: unknown): boolean {
327
- const status = getStatus(error)
328
- if (status === 429) return true
329
- if (status !== undefined) return status >= 500
330
- const message = normalizeReason(error).toLowerCase()
331
- return (
332
- message.includes("network") ||
333
- message.includes("fetch") ||
334
- message.includes("timeout") ||
335
- message.includes("socket") ||
336
- message.includes("econn")
337
- )
338
- }
339
-
340
- async function withRetry<T>(task: () => Promise<T>, deps: Required<Pick<AuthDependencies, "sleep">>): Promise<T> {
341
- let lastError: unknown
342
- for (let attempt = 0; attempt < 3; attempt += 1) {
343
- try {
344
- return await task()
345
- } catch (error) {
346
- lastError = error
347
- if (!isRetryableError(error)) throw error
348
- if (getStatus(error) === 429) {
349
- if (attempt >= 1) throw error
350
- await deps.sleep(getRetryAfterMs(error))
351
- continue
352
- }
353
- if (attempt >= 2) throw error
354
- await deps.sleep(attempt === 0 ? 500 : 1500)
355
- }
356
- }
357
- throw lastError
358
- }
359
-
360
- function normalizeProviderKey(value: string | undefined): string | undefined {
361
- if (!value || value === OAUTH_DUMMY_KEY) return undefined
362
- return value
363
- }
364
-
365
- function ensureOAuthInfo(value: AuthInfo | undefined): OAuthInfo | undefined {
366
- return value && value.type === "oauth" ? value : undefined
367
- }
368
-
369
- async function readAuthMap(deps: AuthDependencies): Promise<Record<string, AuthInfo> | undefined> {
370
- if (process.env.OPENCODE_AUTH_CONTENT) {
371
- try {
372
- const parsed = JSON.parse(process.env.OPENCODE_AUTH_CONTENT) as Record<string, AuthInfo>
373
- if (parsed && typeof parsed === "object") return parsed
374
- } catch {}
375
- return undefined
376
- }
377
-
378
- const filePath = authFilePath()
379
- try {
380
- const fileStat = await (deps.stat ?? stat)(filePath)
381
- if ((fileStat.mode & 0o777) !== 0o600) return undefined
382
- const raw = await (deps.readFile ?? readFile)(filePath, "utf8")
383
- const parsed = JSON.parse(raw) as Record<string, AuthInfo>
384
- return parsed && typeof parsed === "object" ? parsed : undefined
385
- } catch {
386
- return undefined
387
- }
388
- }
389
-
390
- async function refreshAnthropic(
391
- info: OAuthInfo,
392
- deps: Required<Pick<AuthDependencies, "fetchImpl" | "sleep">>,
393
- ): Promise<OAuthInfo> {
394
- const response = await withRetry(
395
- () =>
396
- deps
397
- .fetchImpl("https://console.anthropic.com/v1/oauth/token", {
398
- method: "POST",
399
- headers: {
400
- "Content-Type": "application/json",
401
- },
402
- body: JSON.stringify({
403
- grant_type: "refresh_token",
404
- refresh_token: info.refresh,
405
- client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
406
- }),
407
- })
408
- .then(async (result) => {
409
- if (!result.ok) {
410
- const error = new Error(`HTTP ${result.status}`) as Error & { response?: Response; status?: number }
411
- error.response = result
412
- error.status = result.status
413
- throw error
414
- }
415
- return result
416
- }),
417
- deps,
418
- )
419
-
420
- let body: Record<string, unknown>
421
- try {
422
- body = (await response.json()) as Record<string, unknown>
423
- } catch (error) {
424
- throw buildOAuthRefreshError("anthropic", normalizeReason(error))
425
- }
426
-
427
- if (typeof body.access_token !== "string" || typeof body.refresh_token !== "string") {
428
- throw buildOAuthRefreshError("anthropic", "Invalid token response")
429
- }
430
-
431
- return {
432
- type: "oauth",
433
- access: body.access_token,
434
- refresh: body.refresh_token,
435
- expires: Date.now() + (typeof body.expires_in === "number" ? body.expires_in : 3600) * 1000,
436
- accountId: info.accountId,
437
- enterpriseUrl: info.enterpriseUrl,
438
- }
439
- }
440
-
441
- async function refreshOpenAI(
442
- info: OAuthInfo,
443
- deps: Required<Pick<AuthDependencies, "fetchImpl" | "sleep">>,
444
- ): Promise<OAuthInfo> {
445
- const body = new URLSearchParams({
446
- grant_type: "refresh_token",
447
- refresh_token: info.refresh,
448
- client_id: "app_EMoamEEZ73f0CkXaXp7hrann",
449
- })
450
- const response = await withRetry(
451
- () =>
452
- deps
453
- .fetchImpl("https://auth.openai.com/oauth/token", {
454
- method: "POST",
455
- headers: {
456
- "Content-Type": "application/x-www-form-urlencoded",
457
- },
458
- body,
459
- })
460
- .then(async (result) => {
461
- if (!result.ok) {
462
- const error = new Error(`HTTP ${result.status}`) as Error & { response?: Response; status?: number }
463
- error.response = result
464
- error.status = result.status
465
- throw error
466
- }
467
- return result
468
- }),
469
- deps,
470
- )
471
-
472
- let parsed: Record<string, unknown>
473
- try {
474
- parsed = (await response.json()) as Record<string, unknown>
475
- } catch (error) {
476
- throw buildOAuthRefreshError("openai", normalizeReason(error))
477
- }
478
-
479
- if (typeof parsed.access_token !== "string" || typeof parsed.refresh_token !== "string") {
480
- throw buildOAuthRefreshError("openai", "Invalid token response")
481
- }
482
-
483
- return {
484
- type: "oauth",
485
- access: parsed.access_token,
486
- refresh: parsed.refresh_token,
487
- expires: Date.now() + (typeof parsed.expires_in === "number" ? parsed.expires_in : 3600) * 1000,
488
- accountId: info.accountId,
489
- enterpriseUrl: info.enterpriseUrl,
490
- }
491
- }
492
-
493
- async function exchangeCopilotToken(
494
- info: OAuthInfo,
495
- deps: Required<Pick<AuthDependencies, "fetchImpl" | "sleep">>,
496
- ): Promise<{ token: string }> {
497
- const response = await withRetry(
498
- () =>
499
- deps
500
- .fetchImpl("https://api.github.com/copilot_internal/v2/token", {
501
- method: "GET",
502
- headers: {
503
- Authorization: `token ${info.refresh}`,
504
- },
505
- })
506
- .then(async (result) => {
507
- if (!result.ok) {
508
- const error = new Error(`HTTP ${result.status}`) as Error & { response?: Response; status?: number }
509
- error.response = result
510
- error.status = result.status
511
- throw error
512
- }
513
- return result
514
- }),
515
- deps,
516
- )
517
-
518
- const parsed = (await response.json()) as Record<string, unknown>
519
- if (typeof parsed.token !== "string") {
520
- throw buildOAuthRefreshError("github-copilot", "Invalid token response")
521
- }
522
- return { token: parsed.token }
523
- }
524
-
525
- export function createCredentialResolver(
526
- client: PluginClientLike,
527
- options: ResolvedTranslateOptions,
528
- deps: AuthDependencies = {},
529
- ) {
530
- const fetchImpl = deps.fetchImpl ?? fetch
531
- const now = deps.now ?? (() => Date.now())
532
- const sleepImpl = deps.sleep ?? ((ms: number) => sleep(ms))
533
-
534
- async function getProvider(providerID: string): Promise<ProviderInfo | undefined> {
535
- try {
536
- const listed = unwrapData(await client.provider.list({ throwOnError: true }))
537
- return listed.all.find((provider) => provider.id === providerID)
538
- } catch {
539
- return undefined
540
- }
541
- }
542
-
543
- async function resolveOAuth(providerID: string): Promise<OAuthInfo | undefined> {
544
- const authMap = await readAuthMap(deps)
545
- const info = ensureOAuthInfo(authMap?.[providerID])
546
- if (!info) return undefined
547
- if (info.expires >= now() + 60_000) return info
548
-
549
- const existing = oauthRefreshInflight.get(providerID)
550
- if (existing) return existing
551
-
552
- const refreshPromise = (async () => {
553
- try {
554
- let refreshed: OAuthInfo
555
- try {
556
- if (providerID === "anthropic") {
557
- refreshed = await refreshAnthropic(info, { fetchImpl, sleep: sleepImpl })
558
- } else if (providerID === "openai") {
559
- refreshed = await refreshOpenAI(info, { fetchImpl, sleep: sleepImpl })
560
- } else if (providerID === "github-copilot") {
561
- return info
562
- } else {
563
- return info
564
- }
565
- } catch (error) {
566
- if (error instanceof Error && error.message.includes(":OAUTH_REFRESH_FAILED]")) throw error
567
- throw buildOAuthRefreshError(providerID, normalizeReason(error))
568
- }
569
-
570
- await client.auth.set({
571
- path: { id: providerID },
572
- body: refreshed,
573
- })
574
-
575
- return refreshed
576
- } finally {
577
- oauthRefreshInflight.delete(providerID)
578
- }
579
- })()
580
-
581
- oauthRefreshInflight.set(providerID, refreshPromise)
582
- return refreshPromise
583
- }
584
-
585
- function buildOAuthFetch(providerID: string): FetchLike {
586
- return async (input, init) => {
587
- const info = await resolveOAuth(providerID)
588
- if (!info) return fetchImpl(input, init)
589
-
590
- const headers = copyHeaders(init?.headers)
591
- setUserAgent(headers, deps.packageVersion)
592
- const inputUrl =
593
- input instanceof URL ? new URL(input.href) : new URL(typeof input === "string" ? input : input.url)
594
-
595
- let nextBody = init?.body
596
- let convertCodexResponse = false
597
-
598
- if (providerID === "anthropic") {
599
- // Match the Claude Code CLI fingerprint so Anthropic's OAuth rate-limit
600
- // guard doesn't reject third-party agents. See src/anthropic-oauth.ts.
601
- setAnthropicOAuthHeaders(headers, info.access)
602
- headers.set("anthropic-version", "2023-06-01")
603
- rewriteMessagesURL(inputUrl)
604
- if (isAnthropicMessagesRequest(inputUrl) && typeof nextBody === "string") {
605
- nextBody = rewriteMessagesBody(nextBody)
606
- }
607
- }
608
-
609
- if (providerID === "openai") {
610
- headers.set("Authorization", `Bearer ${info.access}`)
611
- if (info.accountId) headers.set("ChatGPT-Account-Id", info.accountId)
612
- if (
613
- inputUrl.hostname === "api.openai.com" &&
614
- (inputUrl.pathname === "/v1/chat/completions" || inputUrl.pathname === "/v1/responses")
615
- ) {
616
- const rewritten = rewriteOpenAICodexBody(nextBody)
617
- inputUrl.protocol = "https:"
618
- inputUrl.hostname = "chatgpt.com"
619
- inputUrl.pathname = "/backend-api/codex/responses"
620
- inputUrl.search = ""
621
- nextBody = rewritten.body
622
- convertCodexResponse = !rewritten.originalStream
623
- headers.set("OpenAI-Beta", "responses=experimental")
624
- headers.set("originator", "codex_cli_rs")
625
- headers.set("accept", "text/event-stream")
626
- headers.delete("content-length")
627
- }
628
- }
629
-
630
- if (providerID === "github-copilot") {
631
- const session = await exchangeCopilotToken(info, { fetchImpl, sleep: sleepImpl })
632
- headers.set("Authorization", `Bearer ${session.token}`)
633
- headers.set(
634
- "Editor-Version",
635
- deps.packageVersion ? `${USER_AGENT.replace("0.0.0", deps.packageVersion)}` : USER_AGENT,
636
- )
637
- headers.set(
638
- "Editor-Plugin-Version",
639
- deps.packageVersion ? `${USER_AGENT.replace("0.0.0", deps.packageVersion)}` : USER_AGENT,
640
- )
641
- headers.set("Copilot-Integration-Id", "vscode-chat")
642
- headers.delete("x-api-key")
643
-
644
- if (info.enterpriseUrl) {
645
- const target = new URL(
646
- info.enterpriseUrl.includes("://") ? info.enterpriseUrl : `https://${info.enterpriseUrl}`,
647
- )
648
- inputUrl.protocol = target.protocol
649
- inputUrl.hostname = target.hostname
650
- inputUrl.port = target.port
651
- }
652
- }
653
-
654
- const response = await fetchImpl(inputUrl, {
655
- ...init,
656
- headers,
657
- body: nextBody,
658
- })
659
- if (convertCodexResponse && response.ok) return convertCodexSSEToJSON(response)
660
- return response
661
- }
662
- }
663
-
664
- async function resolve(providerModel: string): Promise<ResolvedCredential> {
665
- const { providerID } = parseTranslatorModel(providerModel)
666
- const cached = credentialCache.get(providerID)
667
- if (cached) return cached
668
-
669
- const provider = await getProvider(providerID)
670
-
671
- if (options.apiKey) {
672
- const resolved = { providerID, provider, apiKey: options.apiKey, mode: "apiKey" as const }
673
- credentialCache.set(providerID, resolved)
674
- return resolved
675
- }
676
-
677
- const providerKey = normalizeProviderKey(provider?.key)
678
-
679
- if (provider?.source === "api" && providerKey) {
680
- const resolved = { providerID, provider, apiKey: providerKey, mode: "apiKey" as const }
681
- credentialCache.set(providerID, resolved)
682
- return resolved
683
- }
684
-
685
- if (provider?.source === "env" && providerKey) {
686
- const resolved = { providerID, provider, apiKey: providerKey, mode: "apiKey" as const }
687
- credentialCache.set(providerID, resolved)
688
- return resolved
689
- }
690
-
691
- if (provider?.source === "custom" || provider?.key === OAUTH_DUMMY_KEY) {
692
- const oauthInfo = await resolveOAuth(providerID)
693
- if (oauthInfo) {
694
- const resolved = {
695
- providerID,
696
- provider,
697
- apiKey: "",
698
- fetch: buildOAuthFetch(providerID),
699
- mode: "oauth" as const,
700
- }
701
- credentialCache.set(providerID, resolved)
702
- return resolved
703
- }
704
- }
705
-
706
- if (provider?.key === undefined && (provider?.env.length ?? 0) > 1) {
707
- const resolved = { providerID, provider, mode: "default" as const }
708
- credentialCache.set(providerID, resolved)
709
- return resolved
710
- }
711
-
712
- return { providerID, provider, mode: "default" }
713
- }
714
-
715
- function authUnavailable(providerID: string, provider?: ProviderInfo): Error {
716
- return buildAuthUnavailableError(providerID, getEnvVarHint(provider))
717
- }
718
-
719
- return {
720
- resolve,
721
- authUnavailable,
722
- isMissingCredentialError,
723
- envFallback: AUTH_ENV_FALLBACK,
724
- }
725
- }
1
+ export * from "./auth/index"