opencode-translate 1.0.6 → 1.0.7
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/dist/index.js +2271 -0
- package/index.d.ts +5 -0
- package/package.json +10 -4
- package/src/activation/chat-message.ts +0 -189
- package/src/activation/index.ts +0 -38
- package/src/activation/logging.ts +0 -11
- package/src/activation/messages-transform.ts +0 -44
- package/src/activation/metadata.ts +0 -41
- package/src/activation/parts.ts +0 -46
- package/src/activation/question-hooks.ts +0 -126
- package/src/activation/state.ts +0 -97
- package/src/activation/text-complete.ts +0 -50
- package/src/activation/trigger.ts +0 -57
- package/src/activation/types.ts +0 -47
- package/src/activation.ts +0 -1
- package/src/anthropic-oauth.ts +0 -148
- package/src/auth/codex-request.ts +0 -108
- package/src/auth/codex-response.ts +0 -78
- package/src/auth/codex-shared.ts +0 -3
- package/src/auth/headers.ts +0 -18
- package/src/auth/index.ts +0 -177
- package/src/auth/oauth-fetch.ts +0 -100
- package/src/auth/refresh.ts +0 -102
- package/src/auth/retry.ts +0 -70
- package/src/auth/store.ts +0 -98
- package/src/auth/types.ts +0 -27
- package/src/auth.ts +0 -1
- package/src/constants/errors.ts +0 -24
- package/src/constants/guards.ts +0 -33
- package/src/constants/options.ts +0 -55
- package/src/constants/plugin.ts +0 -9
- package/src/constants/types.ts +0 -159
- package/src/constants.ts +0 -5
- package/src/formatting.ts +0 -157
- package/src/index.ts +0 -7
- package/src/labels.ts +0 -3
- package/src/prompts.ts +0 -123
- package/src/question-tool.ts +0 -234
- package/src/translator/index.ts +0 -172
- package/src/translator/part-id.ts +0 -43
- package/src/translator/provider.ts +0 -411
- package/src/translator/retry.ts +0 -62
- package/src/translator/types.ts +0 -24
- package/src/translator.ts +0 -1
package/src/auth/index.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { setTimeout as sleep } from "node:timers/promises"
|
|
2
|
-
import {
|
|
3
|
-
AUTH_ENV_FALLBACK,
|
|
4
|
-
buildAuthUnavailableError,
|
|
5
|
-
buildOAuthRefreshError,
|
|
6
|
-
getEnvVarHint,
|
|
7
|
-
normalizeReason,
|
|
8
|
-
OAUTH_DUMMY_KEY,
|
|
9
|
-
type OAuthInfo,
|
|
10
|
-
type PluginClientLike,
|
|
11
|
-
type ProviderInfo,
|
|
12
|
-
parseTranslatorModel,
|
|
13
|
-
unwrapData,
|
|
14
|
-
} from "../constants"
|
|
15
|
-
import { buildOAuthFetch } from "./oauth-fetch"
|
|
16
|
-
import { refreshAnthropic, refreshOpenAI } from "./refresh"
|
|
17
|
-
import { ensureOAuthInfo, normalizeProviderKey, readAuthMap } from "./store"
|
|
18
|
-
import type { AuthDependencies, AuthRuntime, ResolvedCredential } from "./types"
|
|
19
|
-
|
|
20
|
-
export function __resetAuthCachesForTest() {
|
|
21
|
-
// Resolver instances own their caches; this remains as a stable test helper.
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function isMissingCredentialError(error: unknown): boolean {
|
|
25
|
-
const message = normalizeReason(error).toLowerCase()
|
|
26
|
-
return (
|
|
27
|
-
message.includes("api key") ||
|
|
28
|
-
message.includes("api-key") ||
|
|
29
|
-
message.includes("missing credentials") ||
|
|
30
|
-
message.includes("missing authentication") ||
|
|
31
|
-
message.includes("missing auth") ||
|
|
32
|
-
message.includes("no auth")
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function hasOAuthRequestAdapter(providerID: string): boolean {
|
|
37
|
-
return providerID === "anthropic" || providerID === "openai" || providerID === "github-copilot"
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function refreshProviderOAuth(
|
|
41
|
-
providerID: string,
|
|
42
|
-
info: OAuthInfo,
|
|
43
|
-
client: PluginClientLike,
|
|
44
|
-
runtime: AuthRuntime,
|
|
45
|
-
) {
|
|
46
|
-
let refreshed: OAuthInfo
|
|
47
|
-
try {
|
|
48
|
-
if (providerID === "anthropic") refreshed = await refreshAnthropic(info, runtime)
|
|
49
|
-
else if (providerID === "openai") refreshed = await refreshOpenAI(info, runtime)
|
|
50
|
-
else return info
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if (error instanceof Error && error.message.includes(":OAUTH_REFRESH_FAILED]")) throw error
|
|
53
|
-
throw buildOAuthRefreshError(providerID, normalizeReason(error))
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
await client.auth.set({ path: { id: providerID }, body: refreshed })
|
|
57
|
-
return refreshed
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function getProvider(client: PluginClientLike, providerID: string): Promise<ProviderInfo | undefined> {
|
|
61
|
-
try {
|
|
62
|
-
const listed = unwrapData(await client.provider.list({ throwOnError: true }))
|
|
63
|
-
return listed.all.find((provider) => provider.id === providerID)
|
|
64
|
-
} catch {
|
|
65
|
-
return undefined
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function createCredentialResolver(client: PluginClientLike, deps: AuthDependencies = {}) {
|
|
70
|
-
const credentialCache = new Map<string, ResolvedCredential>()
|
|
71
|
-
const oauthRefreshInflight = new Map<string, Promise<OAuthInfo>>()
|
|
72
|
-
const runtime: AuthRuntime = {
|
|
73
|
-
fetchImpl: deps.fetchImpl ?? fetch,
|
|
74
|
-
sleep: deps.sleep ?? ((ms: number) => sleep(ms)),
|
|
75
|
-
}
|
|
76
|
-
const now = deps.now ?? (() => Date.now())
|
|
77
|
-
|
|
78
|
-
async function resolveOAuth(providerID: string): Promise<OAuthInfo | undefined> {
|
|
79
|
-
const authMap = await readAuthMap(deps)
|
|
80
|
-
const info = ensureOAuthInfo(authMap?.[providerID])
|
|
81
|
-
if (!info) return undefined
|
|
82
|
-
if (info.expires >= now() + 60_000) return info
|
|
83
|
-
|
|
84
|
-
const inflightKey = `${providerID}:${info.refresh}`
|
|
85
|
-
const existing = oauthRefreshInflight.get(inflightKey)
|
|
86
|
-
if (existing) return existing
|
|
87
|
-
|
|
88
|
-
const refreshPromise = refreshProviderOAuth(providerID, info, client, runtime).finally(() => {
|
|
89
|
-
oauthRefreshInflight.delete(inflightKey)
|
|
90
|
-
})
|
|
91
|
-
oauthRefreshInflight.set(inflightKey, refreshPromise)
|
|
92
|
-
return refreshPromise
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function resolveAuthInfo(providerID: string) {
|
|
96
|
-
return (await readAuthMap(deps))?.[providerID]
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function credentialFromOAuth(
|
|
100
|
-
providerID: string,
|
|
101
|
-
provider: ProviderInfo | undefined,
|
|
102
|
-
authInfo: OAuthInfo,
|
|
103
|
-
): ResolvedCredential {
|
|
104
|
-
return {
|
|
105
|
-
providerID,
|
|
106
|
-
provider,
|
|
107
|
-
authInfo,
|
|
108
|
-
apiKey: "",
|
|
109
|
-
fetch: buildOAuthFetch({ ...runtime, providerID, resolveOAuth, packageVersion: deps.packageVersion }),
|
|
110
|
-
mode: "oauth",
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function resolve(providerModel: string): Promise<ResolvedCredential> {
|
|
115
|
-
const { providerID } = parseTranslatorModel(providerModel)
|
|
116
|
-
const cached = credentialCache.get(providerID)
|
|
117
|
-
if (cached && providerID !== "openai") return cached
|
|
118
|
-
|
|
119
|
-
const provider = await getProvider(client, providerID)
|
|
120
|
-
const authInfo = await resolveAuthInfo(providerID)
|
|
121
|
-
if (providerID === "openai" && authInfo?.type === "oauth") {
|
|
122
|
-
const oauthInfo = await resolveOAuth(providerID)
|
|
123
|
-
if (oauthInfo) {
|
|
124
|
-
const resolved = credentialFromOAuth(providerID, provider, oauthInfo)
|
|
125
|
-
credentialCache.set(providerID, resolved)
|
|
126
|
-
return resolved
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (cached && !(providerID === "openai" && cached.mode === "oauth" && authInfo?.type !== "oauth")) return cached
|
|
130
|
-
|
|
131
|
-
const providerKey = normalizeProviderKey(provider?.key)
|
|
132
|
-
if (providerKey) {
|
|
133
|
-
const resolved = { providerID, provider, authInfo, apiKey: providerKey, mode: "apiKey" as const }
|
|
134
|
-
credentialCache.set(providerID, resolved)
|
|
135
|
-
return resolved
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (authInfo?.type === "api" && authInfo.key) {
|
|
139
|
-
const resolved = { providerID, provider, authInfo, apiKey: authInfo.key, mode: "apiKey" as const }
|
|
140
|
-
credentialCache.set(providerID, resolved)
|
|
141
|
-
return resolved
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (provider?.source === "custom" || provider?.key === OAUTH_DUMMY_KEY || hasOAuthRequestAdapter(providerID)) {
|
|
145
|
-
const oauthInfo = await resolveOAuth(providerID)
|
|
146
|
-
if (oauthInfo) {
|
|
147
|
-
const resolved = credentialFromOAuth(providerID, provider, oauthInfo)
|
|
148
|
-
credentialCache.set(providerID, resolved)
|
|
149
|
-
return resolved
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (authInfo?.type === "oauth" && authInfo.access && provider?.options?.apiKey === undefined) {
|
|
154
|
-
const resolved = { providerID, provider, authInfo, apiKey: authInfo.access, mode: "oauth" as const }
|
|
155
|
-
credentialCache.set(providerID, resolved)
|
|
156
|
-
return resolved
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (provider?.key === undefined && (provider?.env.length ?? 0) > 1) {
|
|
160
|
-
const resolved = { providerID, provider, authInfo, mode: "default" as const }
|
|
161
|
-
credentialCache.set(providerID, resolved)
|
|
162
|
-
return resolved
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return { providerID, provider, authInfo, mode: "default" }
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
resolve,
|
|
170
|
-
authUnavailable: (providerID: string, provider?: ProviderInfo) =>
|
|
171
|
-
buildAuthUnavailableError(providerID, getEnvVarHint(provider)),
|
|
172
|
-
isMissingCredentialError,
|
|
173
|
-
envFallback: AUTH_ENV_FALLBACK,
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export type { AuthDependencies, ResolvedCredential }
|
package/src/auth/oauth-fetch.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isAnthropicMessagesRequest,
|
|
3
|
-
rewriteMessagesBody,
|
|
4
|
-
rewriteMessagesURL,
|
|
5
|
-
setOAuthHeaders as setAnthropicOAuthHeaders,
|
|
6
|
-
} from "../anthropic-oauth"
|
|
7
|
-
import type { FetchLike, OAuthInfo } from "../constants"
|
|
8
|
-
import { rewriteOpenAICodexBody } from "./codex-request"
|
|
9
|
-
import { convertCodexSSEToJSON } from "./codex-response"
|
|
10
|
-
import { copyHeaders, packageUserAgent, setUserAgent } from "./headers"
|
|
11
|
-
import { exchangeCopilotToken } from "./refresh"
|
|
12
|
-
import type { AuthRuntime, OAuthResolver } from "./types"
|
|
13
|
-
|
|
14
|
-
interface OAuthFetchOptions extends AuthRuntime {
|
|
15
|
-
providerID: string
|
|
16
|
-
resolveOAuth: OAuthResolver
|
|
17
|
-
packageVersion?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface RequestState {
|
|
21
|
-
headers: Headers
|
|
22
|
-
inputUrl: URL
|
|
23
|
-
body: BodyInit | null | undefined
|
|
24
|
-
convertCodexResponse: boolean
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function applyAnthropicRequest(state: RequestState, info: OAuthInfo) {
|
|
28
|
-
setAnthropicOAuthHeaders(state.headers, info.access)
|
|
29
|
-
state.headers.set("anthropic-version", "2023-06-01")
|
|
30
|
-
rewriteMessagesURL(state.inputUrl)
|
|
31
|
-
if (isAnthropicMessagesRequest(state.inputUrl) && typeof state.body === "string") {
|
|
32
|
-
state.body = rewriteMessagesBody(state.body)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function applyOpenAIRequest(state: RequestState, info: OAuthInfo) {
|
|
37
|
-
state.headers.set("Authorization", `Bearer ${info.access}`)
|
|
38
|
-
if (info.accountId) state.headers.set("ChatGPT-Account-Id", info.accountId)
|
|
39
|
-
if (
|
|
40
|
-
state.inputUrl.hostname !== "api.openai.com" ||
|
|
41
|
-
(state.inputUrl.pathname !== "/v1/chat/completions" && state.inputUrl.pathname !== "/v1/responses")
|
|
42
|
-
) {
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const rewritten = rewriteOpenAICodexBody(state.body)
|
|
47
|
-
state.inputUrl.protocol = "https:"
|
|
48
|
-
state.inputUrl.hostname = "chatgpt.com"
|
|
49
|
-
state.inputUrl.pathname = "/backend-api/codex/responses"
|
|
50
|
-
state.inputUrl.search = ""
|
|
51
|
-
state.body = rewritten.body
|
|
52
|
-
state.convertCodexResponse = !rewritten.originalStream
|
|
53
|
-
state.headers.set("OpenAI-Beta", "responses=experimental")
|
|
54
|
-
state.headers.set("originator", "codex_cli_rs")
|
|
55
|
-
state.headers.set("accept", "text/event-stream")
|
|
56
|
-
state.headers.delete("content-length")
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function applyCopilotRequest(state: RequestState, info: OAuthInfo, options: OAuthFetchOptions) {
|
|
60
|
-
const session = await exchangeCopilotToken(info, options)
|
|
61
|
-
state.headers.set("Authorization", `Bearer ${session.token}`)
|
|
62
|
-
state.headers.set("Editor-Version", packageUserAgent(options.packageVersion))
|
|
63
|
-
state.headers.set("Editor-Plugin-Version", packageUserAgent(options.packageVersion))
|
|
64
|
-
state.headers.set("Copilot-Integration-Id", "vscode-chat")
|
|
65
|
-
state.headers.delete("x-api-key")
|
|
66
|
-
|
|
67
|
-
if (info.enterpriseUrl) {
|
|
68
|
-
const target = new URL(info.enterpriseUrl.includes("://") ? info.enterpriseUrl : `https://${info.enterpriseUrl}`)
|
|
69
|
-
state.inputUrl.protocol = target.protocol
|
|
70
|
-
state.inputUrl.hostname = target.hostname
|
|
71
|
-
state.inputUrl.port = target.port
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function inputToURL(input: RequestInfo | URL): URL {
|
|
76
|
-
return input instanceof URL ? new URL(input.href) : new URL(typeof input === "string" ? input : input.url)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function buildOAuthFetch(options: OAuthFetchOptions): FetchLike {
|
|
80
|
-
return async (input, init) => {
|
|
81
|
-
const info = await options.resolveOAuth(options.providerID)
|
|
82
|
-
if (!info) return options.fetchImpl(input, init)
|
|
83
|
-
|
|
84
|
-
const state: RequestState = {
|
|
85
|
-
headers: copyHeaders(init?.headers),
|
|
86
|
-
inputUrl: inputToURL(input),
|
|
87
|
-
body: init?.body,
|
|
88
|
-
convertCodexResponse: false,
|
|
89
|
-
}
|
|
90
|
-
setUserAgent(state.headers, options.packageVersion)
|
|
91
|
-
|
|
92
|
-
if (options.providerID === "anthropic") applyAnthropicRequest(state, info)
|
|
93
|
-
if (options.providerID === "openai") applyOpenAIRequest(state, info)
|
|
94
|
-
if (options.providerID === "github-copilot") await applyCopilotRequest(state, info, options)
|
|
95
|
-
|
|
96
|
-
const response = await options.fetchImpl(state.inputUrl, { ...init, headers: state.headers, body: state.body })
|
|
97
|
-
if (state.convertCodexResponse && response.ok) return convertCodexSSEToJSON(response)
|
|
98
|
-
return response
|
|
99
|
-
}
|
|
100
|
-
}
|
package/src/auth/refresh.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { buildOAuthRefreshError, normalizeReason, type OAuthInfo } from "../constants"
|
|
2
|
-
import { withRetry } from "./retry"
|
|
3
|
-
import type { AuthRuntime } from "./types"
|
|
4
|
-
|
|
5
|
-
async function postOAuthToken(url: string, init: RequestInit, deps: AuthRuntime): Promise<Response> {
|
|
6
|
-
return withRetry(
|
|
7
|
-
() =>
|
|
8
|
-
deps.fetchImpl(url, init).then(async (result) => {
|
|
9
|
-
if (!result.ok) {
|
|
10
|
-
const error = new Error(`HTTP ${result.status}`) as Error & { response?: Response; status?: number }
|
|
11
|
-
error.response = result
|
|
12
|
-
error.status = result.status
|
|
13
|
-
throw error
|
|
14
|
-
}
|
|
15
|
-
return result
|
|
16
|
-
}),
|
|
17
|
-
deps,
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function refreshAnthropic(info: OAuthInfo, deps: AuthRuntime): Promise<OAuthInfo> {
|
|
22
|
-
const response = await postOAuthToken(
|
|
23
|
-
"https://console.anthropic.com/v1/oauth/token",
|
|
24
|
-
{
|
|
25
|
-
method: "POST",
|
|
26
|
-
headers: { "Content-Type": "application/json" },
|
|
27
|
-
body: JSON.stringify({
|
|
28
|
-
grant_type: "refresh_token",
|
|
29
|
-
refresh_token: info.refresh,
|
|
30
|
-
client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
31
|
-
}),
|
|
32
|
-
},
|
|
33
|
-
deps,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
let body: Record<string, unknown>
|
|
37
|
-
try {
|
|
38
|
-
body = (await response.json()) as Record<string, unknown>
|
|
39
|
-
} catch (error) {
|
|
40
|
-
throw buildOAuthRefreshError("anthropic", normalizeReason(error))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (typeof body.access_token !== "string" || typeof body.refresh_token !== "string") {
|
|
44
|
-
throw buildOAuthRefreshError("anthropic", "Invalid token response")
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
type: "oauth",
|
|
49
|
-
access: body.access_token,
|
|
50
|
-
refresh: body.refresh_token,
|
|
51
|
-
expires: Date.now() + (typeof body.expires_in === "number" ? body.expires_in : 3600) * 1000,
|
|
52
|
-
accountId: info.accountId,
|
|
53
|
-
enterpriseUrl: info.enterpriseUrl,
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function refreshOpenAI(info: OAuthInfo, deps: AuthRuntime): Promise<OAuthInfo> {
|
|
58
|
-
const response = await postOAuthToken(
|
|
59
|
-
"https://auth.openai.com/oauth/token",
|
|
60
|
-
{
|
|
61
|
-
method: "POST",
|
|
62
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
63
|
-
body: new URLSearchParams({
|
|
64
|
-
grant_type: "refresh_token",
|
|
65
|
-
refresh_token: info.refresh,
|
|
66
|
-
client_id: "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
67
|
-
}),
|
|
68
|
-
},
|
|
69
|
-
deps,
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
let parsed: Record<string, unknown>
|
|
73
|
-
try {
|
|
74
|
-
parsed = (await response.json()) as Record<string, unknown>
|
|
75
|
-
} catch (error) {
|
|
76
|
-
throw buildOAuthRefreshError("openai", normalizeReason(error))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (typeof parsed.access_token !== "string" || typeof parsed.refresh_token !== "string") {
|
|
80
|
-
throw buildOAuthRefreshError("openai", "Invalid token response")
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
type: "oauth",
|
|
85
|
-
access: parsed.access_token,
|
|
86
|
-
refresh: parsed.refresh_token,
|
|
87
|
-
expires: Date.now() + (typeof parsed.expires_in === "number" ? parsed.expires_in : 3600) * 1000,
|
|
88
|
-
accountId: info.accountId,
|
|
89
|
-
enterpriseUrl: info.enterpriseUrl,
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function exchangeCopilotToken(info: OAuthInfo, deps: AuthRuntime): Promise<{ token: string }> {
|
|
94
|
-
const response = await postOAuthToken(
|
|
95
|
-
"https://api.github.com/copilot_internal/v2/token",
|
|
96
|
-
{ method: "GET", headers: { Authorization: `token ${info.refresh}` } },
|
|
97
|
-
deps,
|
|
98
|
-
)
|
|
99
|
-
const parsed = (await response.json()) as Record<string, unknown>
|
|
100
|
-
if (typeof parsed.token !== "string") throw buildOAuthRefreshError("github-copilot", "Invalid token response")
|
|
101
|
-
return { token: parsed.token }
|
|
102
|
-
}
|
package/src/auth/retry.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { normalizeReason } from "../constants"
|
|
2
|
-
import { headerValue } from "./headers"
|
|
3
|
-
import type { AuthDependencies } from "./types"
|
|
4
|
-
|
|
5
|
-
function getStatus(error: unknown): number | undefined {
|
|
6
|
-
if (!error || typeof error !== "object") return undefined
|
|
7
|
-
const record = error as Record<string, unknown>
|
|
8
|
-
if (typeof record.status === "number") return record.status
|
|
9
|
-
if (typeof record.statusCode === "number") return record.statusCode
|
|
10
|
-
const response = record.response
|
|
11
|
-
if (response && typeof response === "object") {
|
|
12
|
-
const maybeStatus = (response as Record<string, unknown>).status
|
|
13
|
-
if (typeof maybeStatus === "number") return maybeStatus
|
|
14
|
-
}
|
|
15
|
-
return undefined
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getRetryAfterMs(error: unknown): number {
|
|
19
|
-
if (!error || typeof error !== "object") return 2000
|
|
20
|
-
const response = (error as Record<string, unknown>).response
|
|
21
|
-
if (response && typeof response === "object") {
|
|
22
|
-
const headers = (response as { headers?: Headers }).headers
|
|
23
|
-
if (headers instanceof Headers) {
|
|
24
|
-
const retryAfter = headerValue(headers, "retry-after")
|
|
25
|
-
if (!retryAfter) return 2000
|
|
26
|
-
const seconds = Number(retryAfter)
|
|
27
|
-
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000)
|
|
28
|
-
const date = Date.parse(retryAfter)
|
|
29
|
-
if (Number.isFinite(date)) return Math.max(0, date - Date.now())
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return 2000
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isRetryableError(error: unknown): boolean {
|
|
36
|
-
const status = getStatus(error)
|
|
37
|
-
if (status === 429) return true
|
|
38
|
-
if (status !== undefined) return status >= 500
|
|
39
|
-
const message = normalizeReason(error).toLowerCase()
|
|
40
|
-
return (
|
|
41
|
-
message.includes("network") ||
|
|
42
|
-
message.includes("fetch") ||
|
|
43
|
-
message.includes("timeout") ||
|
|
44
|
-
message.includes("socket") ||
|
|
45
|
-
message.includes("econn")
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function withRetry<T>(
|
|
50
|
-
task: () => Promise<T>,
|
|
51
|
-
deps: Required<Pick<AuthDependencies, "sleep">>,
|
|
52
|
-
): Promise<T> {
|
|
53
|
-
let lastError: unknown
|
|
54
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
55
|
-
try {
|
|
56
|
-
return await task()
|
|
57
|
-
} catch (error) {
|
|
58
|
-
lastError = error
|
|
59
|
-
if (!isRetryableError(error)) throw error
|
|
60
|
-
if (getStatus(error) === 429) {
|
|
61
|
-
if (attempt >= 1) throw error
|
|
62
|
-
await deps.sleep(getRetryAfterMs(error))
|
|
63
|
-
continue
|
|
64
|
-
}
|
|
65
|
-
if (attempt >= 2) throw error
|
|
66
|
-
await deps.sleep(attempt === 0 ? 500 : 1500)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
throw lastError
|
|
70
|
-
}
|
package/src/auth/store.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises"
|
|
2
|
-
import os from "node:os"
|
|
3
|
-
import path from "node:path"
|
|
4
|
-
import { type AuthInfo, OAUTH_DUMMY_KEY, type OAuthInfo } from "../constants"
|
|
5
|
-
import type { AuthDependencies } from "./types"
|
|
6
|
-
|
|
7
|
-
// Mirrors opencode's xdg-basedir data location; see packages/core/src/global.ts.
|
|
8
|
-
function dataHome(): string {
|
|
9
|
-
const xdgDataHome = process.env.XDG_DATA_HOME
|
|
10
|
-
if (xdgDataHome) return xdgDataHome
|
|
11
|
-
return path.join(os.homedir(), ".local", "share")
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function authFilePaths(): string[] {
|
|
15
|
-
const root = path.join(dataHome(), "opencode")
|
|
16
|
-
return [path.join(root, "auth.json"), path.join(root, "auth-v2.json")]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function normalizeProviderKey(value: string | undefined): string | undefined {
|
|
20
|
-
if (!value || value === OAUTH_DUMMY_KEY) return undefined
|
|
21
|
-
return value
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function ensureOAuthInfo(value: AuthInfo | undefined): OAuthInfo | undefined {
|
|
25
|
-
return value && value.type === "oauth" ? value : undefined
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isAuthInfo(value: unknown): value is AuthInfo {
|
|
29
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false
|
|
30
|
-
const record = value as Record<string, unknown>
|
|
31
|
-
if (record.type === "api") return typeof record.key === "string"
|
|
32
|
-
if (record.type === "oauth") {
|
|
33
|
-
return typeof record.access === "string" && typeof record.refresh === "string" && typeof record.expires === "number"
|
|
34
|
-
}
|
|
35
|
-
if (record.type === "wellknown") return typeof record.key === "string" && typeof record.token === "string"
|
|
36
|
-
return false
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function normalizeAuthMap(raw: unknown): Record<string, AuthInfo> | undefined {
|
|
40
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined
|
|
41
|
-
const record = raw as Record<string, unknown>
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
record.version === 2 &&
|
|
45
|
-
record.accounts &&
|
|
46
|
-
typeof record.accounts === "object" &&
|
|
47
|
-
!Array.isArray(record.accounts)
|
|
48
|
-
) {
|
|
49
|
-
const accounts = record.accounts as Record<string, unknown>
|
|
50
|
-
const active =
|
|
51
|
-
record.active && typeof record.active === "object" && !Array.isArray(record.active) ? record.active : {}
|
|
52
|
-
const result: Record<string, AuthInfo> = {}
|
|
53
|
-
|
|
54
|
-
for (const [serviceID, accountID] of Object.entries(active as Record<string, unknown>)) {
|
|
55
|
-
if (typeof accountID !== "string") continue
|
|
56
|
-
const account = accounts[accountID]
|
|
57
|
-
if (!account || typeof account !== "object" || Array.isArray(account)) continue
|
|
58
|
-
const credential = (account as Record<string, unknown>).credential
|
|
59
|
-
if (isAuthInfo(credential)) result[serviceID] = credential
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
for (const account of Object.values(accounts)) {
|
|
63
|
-
if (!account || typeof account !== "object" || Array.isArray(account)) continue
|
|
64
|
-
const accountRecord = account as Record<string, unknown>
|
|
65
|
-
const serviceID = accountRecord.serviceID
|
|
66
|
-
const credential = accountRecord.credential
|
|
67
|
-
if (typeof serviceID === "string" && result[serviceID] === undefined && isAuthInfo(credential)) {
|
|
68
|
-
result[serviceID] = credential
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return result
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const result: Record<string, AuthInfo> = {}
|
|
76
|
-
for (const [providerID, info] of Object.entries(record)) {
|
|
77
|
-
if (isAuthInfo(info)) result[providerID] = info
|
|
78
|
-
}
|
|
79
|
-
return result
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function readAuthMap(deps: AuthDependencies): Promise<Record<string, AuthInfo> | undefined> {
|
|
83
|
-
if (process.env.OPENCODE_AUTH_CONTENT) {
|
|
84
|
-
try {
|
|
85
|
-
return normalizeAuthMap(JSON.parse(process.env.OPENCODE_AUTH_CONTENT))
|
|
86
|
-
} catch {}
|
|
87
|
-
return undefined
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
for (const filePath of authFilePaths()) {
|
|
91
|
-
try {
|
|
92
|
-
const raw = await (deps.readFile ?? readFile)(filePath, "utf8")
|
|
93
|
-
const parsed = normalizeAuthMap(JSON.parse(raw))
|
|
94
|
-
if (parsed) return parsed
|
|
95
|
-
} catch {}
|
|
96
|
-
}
|
|
97
|
-
return undefined
|
|
98
|
-
}
|
package/src/auth/types.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { AuthInfo, FetchLike, OAuthInfo, ProviderInfo } from "../constants"
|
|
2
|
-
|
|
3
|
-
export interface ResolvedCredential {
|
|
4
|
-
providerID: string
|
|
5
|
-
provider?: ProviderInfo
|
|
6
|
-
authInfo?: AuthInfo
|
|
7
|
-
apiKey?: string
|
|
8
|
-
fetch?: FetchLike
|
|
9
|
-
mode: "apiKey" | "oauth" | "default"
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface AuthDependencies {
|
|
13
|
-
fetchImpl?: FetchLike
|
|
14
|
-
now?: () => number
|
|
15
|
-
sleep?: (ms: number) => Promise<void>
|
|
16
|
-
readFile?: (filePath: string, encoding: BufferEncoding) => Promise<string>
|
|
17
|
-
packageVersion?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type AuthRuntime = Required<Pick<AuthDependencies, "fetchImpl" | "sleep">>
|
|
21
|
-
|
|
22
|
-
export type OAuthResolver = (providerID: string) => Promise<OAuthInfo | undefined>
|
|
23
|
-
|
|
24
|
-
export interface CodexBodyRewrite {
|
|
25
|
-
body: BodyInit | null | undefined
|
|
26
|
-
originalStream: boolean
|
|
27
|
-
}
|
package/src/auth.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./auth/index"
|
package/src/constants/errors.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { PLUGIN_NAME } from "./plugin"
|
|
2
|
-
|
|
3
|
-
export function normalizeReason(error: unknown): string {
|
|
4
|
-
const raw = error instanceof Error ? error.message : String(error)
|
|
5
|
-
return raw.split(/\r?\n/, 1)[0].trim().slice(0, 200)
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function buildInboundTranslationError(userLanguage: string, reason: string): Error {
|
|
9
|
-
return new Error(
|
|
10
|
-
`[${PLUGIN_NAME}:INBOUND_TRANSLATION_FAILED] Failed to translate user message from ${userLanguage} to English: ${reason}`,
|
|
11
|
-
)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function buildAuthUnavailableError(providerID: string, envVar: string): Error {
|
|
15
|
-
return new Error(
|
|
16
|
-
`[${PLUGIN_NAME}:AUTH_UNAVAILABLE] No credential found for provider "${providerID}". Set ${envVar} in the environment or run "opencode auth login ${providerID}".`,
|
|
17
|
-
)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function buildOAuthRefreshError(providerID: string, reason: string): Error {
|
|
21
|
-
return new Error(
|
|
22
|
-
`[${PLUGIN_NAME}:OAUTH_REFRESH_FAILED] Failed to refresh OAuth token for provider "${providerID}": ${reason}. Re-authenticate with "opencode auth login ${providerID}".`,
|
|
23
|
-
)
|
|
24
|
-
}
|
package/src/constants/guards.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { LLM_LANGUAGE, NONCE_PATTERN } from "./plugin"
|
|
2
|
-
import type { SDKResponseLike, TextPartLike, TranslateState } from "./types"
|
|
3
|
-
|
|
4
|
-
function isNonEmptyString(value: unknown): value is string {
|
|
5
|
-
return typeof value === "string" && value.length > 0
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function unwrapData<T>(value: T | SDKResponseLike<T>): T {
|
|
9
|
-
if (value && typeof value === "object" && "data" in value && (value as SDKResponseLike<T>).data !== undefined) {
|
|
10
|
-
return (value as SDKResponseLike<T>).data as T
|
|
11
|
-
}
|
|
12
|
-
return value as T
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function isTranslateStateRecord(value: unknown): value is TranslateState {
|
|
16
|
-
if (!value || typeof value !== "object") return false
|
|
17
|
-
const record = value as Record<string, unknown>
|
|
18
|
-
return (
|
|
19
|
-
record.translate_enabled === true &&
|
|
20
|
-
record.translate_llm_lang === LLM_LANGUAGE &&
|
|
21
|
-
isNonEmptyString(record.translate_user_lang) &&
|
|
22
|
-
isNonEmptyString(record.translate_nonce) &&
|
|
23
|
-
NONCE_PATTERN.test(record.translate_nonce)
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function isTextPart(part: TextPartLike): part is TextPartLike & { text: string } {
|
|
28
|
-
return part.type === "text" && typeof part.text === "string"
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function isUserAuthoredTextPart(part: TextPartLike): part is TextPartLike & { text: string } {
|
|
32
|
-
return isTextPart(part) && part.synthetic !== true && part.ignored !== true
|
|
33
|
-
}
|