opencode-translate 1.0.5 → 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 -164
- 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/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
|
-
}
|
package/src/constants/options.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { AUTH_ENV_FALLBACK, DEFAULT_TRIGGER, PLUGIN_NAME } from "./plugin"
|
|
2
|
-
import type { ProviderInfo, ResolvedTranslateOptions } from "./types"
|
|
3
|
-
|
|
4
|
-
export function resolveOptions(options: Record<string, unknown>): ResolvedTranslateOptions {
|
|
5
|
-
const model = typeof options.model === "string" ? options.model.trim() : ""
|
|
6
|
-
if (!model) {
|
|
7
|
-
throw new Error(
|
|
8
|
-
`[${PLUGIN_NAME}:INVALID_OPTIONS] options.model is required. Set it to the translator model, e.g. "anthropic/claude-haiku-4-5".`,
|
|
9
|
-
)
|
|
10
|
-
}
|
|
11
|
-
const slash = model.indexOf("/")
|
|
12
|
-
if (slash < 1 || slash === model.length - 1) {
|
|
13
|
-
throw new Error(
|
|
14
|
-
`[${PLUGIN_NAME}:INVALID_OPTIONS] options.model must be in provider/model-id form, e.g. "anthropic/claude-haiku-4-5".`,
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const lang = typeof options.lang === "string" ? options.lang.trim() : ""
|
|
19
|
-
if (!lang) {
|
|
20
|
-
throw new Error(
|
|
21
|
-
`[${PLUGIN_NAME}:INVALID_OPTIONS] options.lang is required. Set it to the user's language, e.g. "Korean" or "Japanese".`,
|
|
22
|
-
)
|
|
23
|
-
}
|
|
24
|
-
const variant = typeof options.variant === "string" ? options.variant.trim() : ""
|
|
25
|
-
|
|
26
|
-
const rawTrigger = Array.isArray(options.trigger)
|
|
27
|
-
? options.trigger
|
|
28
|
-
: Array.isArray(options.triggerKeywords)
|
|
29
|
-
? options.triggerKeywords
|
|
30
|
-
: DEFAULT_TRIGGER
|
|
31
|
-
const trigger = rawTrigger.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
model,
|
|
35
|
-
...(variant ? { variant } : {}),
|
|
36
|
-
trigger: trigger.length > 0 ? trigger : [...DEFAULT_TRIGGER],
|
|
37
|
-
lang,
|
|
38
|
-
verbose: options.verbose === true,
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function getEnvVarHint(provider: ProviderInfo | undefined): string {
|
|
43
|
-
return provider?.env[0] || AUTH_ENV_FALLBACK
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function parseTranslatorModel(model: string): { providerID: string; modelID: string } {
|
|
47
|
-
const slash = model.indexOf("/")
|
|
48
|
-
if (slash < 1 || slash === model.length - 1) {
|
|
49
|
-
return { providerID: "anthropic", modelID: model }
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
providerID: model.slice(0, slash),
|
|
53
|
-
modelID: model.slice(slash + 1),
|
|
54
|
-
}
|
|
55
|
-
}
|
package/src/constants/plugin.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export const PLUGIN_NAME = "opencode-translate"
|
|
2
|
-
export const SPEC_VERSION = 2
|
|
3
|
-
export const LLM_LANGUAGE = "English"
|
|
4
|
-
export const DEFAULT_TRIGGER = ["$en"]
|
|
5
|
-
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
|
|
6
|
-
export const NONCE_PATTERN = /^[0-9a-f]{32}$/
|
|
7
|
-
export const FAILURE_NOTICE = "_Translation unavailable for this segment._"
|
|
8
|
-
export const AUTH_ENV_FALLBACK = "the provider's API key env var"
|
|
9
|
-
export const USER_AGENT = `${PLUGIN_NAME}/0.0.0`
|
package/src/constants/types.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import type { LLM_LANGUAGE } from "./plugin"
|
|
2
|
-
|
|
3
|
-
export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
4
|
-
|
|
5
|
-
type ProviderSource = "env" | "config" | "custom" | "api"
|
|
6
|
-
|
|
7
|
-
export interface ResolvedTranslateOptions {
|
|
8
|
-
model: string
|
|
9
|
-
variant?: string
|
|
10
|
-
trigger: string[]
|
|
11
|
-
lang: string
|
|
12
|
-
verbose: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface TranslateState {
|
|
16
|
-
translate_enabled: true
|
|
17
|
-
translate_user_lang: string
|
|
18
|
-
translate_llm_lang: typeof LLM_LANGUAGE
|
|
19
|
-
translate_nonce: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface StoredTextMetadata extends Record<string, unknown> {
|
|
23
|
-
translate_enabled?: boolean
|
|
24
|
-
translate_user_lang?: string
|
|
25
|
-
translate_llm_lang?: string
|
|
26
|
-
translate_nonce?: string
|
|
27
|
-
translate_role?: string
|
|
28
|
-
translate_spec_version?: number
|
|
29
|
-
translate_source_hash?: string
|
|
30
|
-
translate_en?: string
|
|
31
|
-
translate_part_index?: number
|
|
32
|
-
compaction_continue?: boolean
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface SessionLike {
|
|
36
|
-
id: string
|
|
37
|
-
parentID?: string | null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface MessageLike {
|
|
41
|
-
id: string
|
|
42
|
-
sessionID: string
|
|
43
|
-
role: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface TextPartLike {
|
|
47
|
-
id: string
|
|
48
|
-
sessionID: string
|
|
49
|
-
messageID: string
|
|
50
|
-
type: string
|
|
51
|
-
text?: string
|
|
52
|
-
synthetic?: boolean
|
|
53
|
-
ignored?: boolean
|
|
54
|
-
metadata?: Record<string, unknown>
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface MessageWithPartsLike {
|
|
58
|
-
info: MessageLike
|
|
59
|
-
parts: TextPartLike[]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface ProviderModelInfo {
|
|
63
|
-
id?: string
|
|
64
|
-
api?: {
|
|
65
|
-
id?: string
|
|
66
|
-
url?: string
|
|
67
|
-
npm?: string
|
|
68
|
-
}
|
|
69
|
-
headers?: Record<string, string>
|
|
70
|
-
options?: Record<string, unknown>
|
|
71
|
-
variants?: Record<string, Record<string, unknown>>
|
|
72
|
-
capabilities?: {
|
|
73
|
-
temperature?: boolean
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface ProviderInfo {
|
|
78
|
-
id: string
|
|
79
|
-
source: ProviderSource
|
|
80
|
-
env: string[]
|
|
81
|
-
key?: string
|
|
82
|
-
options?: Record<string, unknown>
|
|
83
|
-
models?: Record<string, ProviderModelInfo>
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
interface ProviderListResponseLike {
|
|
87
|
-
all: ProviderInfo[]
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
interface ApiAuthInfo {
|
|
91
|
-
type: "api"
|
|
92
|
-
key: string
|
|
93
|
-
metadata?: Record<string, string>
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface OAuthInfo {
|
|
97
|
-
type: "oauth"
|
|
98
|
-
refresh: string
|
|
99
|
-
access: string
|
|
100
|
-
expires: number
|
|
101
|
-
accountId?: string
|
|
102
|
-
enterpriseUrl?: string
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
interface WellKnownInfo {
|
|
106
|
-
type: "wellknown"
|
|
107
|
-
key: string
|
|
108
|
-
token: string
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export type AuthInfo = ApiAuthInfo | OAuthInfo | WellKnownInfo
|
|
112
|
-
|
|
113
|
-
export interface SDKResponseLike<T> {
|
|
114
|
-
data?: T
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface PluginClientLike {
|
|
118
|
-
session: {
|
|
119
|
-
get(
|
|
120
|
-
input: (
|
|
121
|
-
| { sessionID: string; directory?: string; workspace?: string }
|
|
122
|
-
| { path: { id: string }; query?: { directory?: string; workspace?: string } }
|
|
123
|
-
) & { throwOnError?: boolean },
|
|
124
|
-
options?: { throwOnError?: boolean },
|
|
125
|
-
): Promise<SessionLike | SDKResponseLike<SessionLike>>
|
|
126
|
-
messages(
|
|
127
|
-
input: (
|
|
128
|
-
| { sessionID: string; directory?: string; workspace?: string }
|
|
129
|
-
| { path: { id: string }; query?: { directory?: string; workspace?: string; limit?: number; before?: string } }
|
|
130
|
-
) & { throwOnError?: boolean },
|
|
131
|
-
options?: { throwOnError?: boolean },
|
|
132
|
-
): Promise<MessageWithPartsLike[] | SDKResponseLike<MessageWithPartsLike[]>>
|
|
133
|
-
message(
|
|
134
|
-
input: (
|
|
135
|
-
| { sessionID: string; messageID: string; directory?: string; workspace?: string }
|
|
136
|
-
| { path: { id: string; messageID: string }; query?: { directory?: string; workspace?: string } }
|
|
137
|
-
) & { throwOnError?: boolean },
|
|
138
|
-
options?: { throwOnError?: boolean },
|
|
139
|
-
): Promise<MessageWithPartsLike | SDKResponseLike<MessageWithPartsLike>>
|
|
140
|
-
}
|
|
141
|
-
provider: {
|
|
142
|
-
list(options?: {
|
|
143
|
-
throwOnError?: boolean
|
|
144
|
-
}): Promise<ProviderListResponseLike | SDKResponseLike<ProviderListResponseLike>>
|
|
145
|
-
}
|
|
146
|
-
auth: {
|
|
147
|
-
set(input: { path: { id: string }; body: AuthInfo }): Promise<unknown>
|
|
148
|
-
}
|
|
149
|
-
app: {
|
|
150
|
-
log(input: {
|
|
151
|
-
body: {
|
|
152
|
-
service: string
|
|
153
|
-
level: string
|
|
154
|
-
message: string
|
|
155
|
-
extra?: Record<string, unknown>
|
|
156
|
-
}
|
|
157
|
-
}): Promise<unknown>
|
|
158
|
-
}
|
|
159
|
-
}
|