opencode-translate 0.1.2 → 0.1.3
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/package.json +6 -3
- package/src/activation/chat-message.ts +164 -0
- package/src/activation/index.ts +38 -0
- package/src/activation/logging.ts +11 -0
- package/src/activation/messages-transform.ts +38 -0
- package/src/activation/metadata.ts +42 -0
- package/src/activation/parts.ts +53 -0
- package/src/activation/question-hooks.ts +95 -0
- package/src/activation/state.ts +98 -0
- package/src/activation/text-complete.ts +51 -0
- package/src/activation/trigger.ts +57 -0
- package/src/activation/types.ts +41 -0
- package/src/activation.ts +1 -633
- package/src/anthropic-oauth.ts +3 -3
- package/src/auth/codex-request.ts +108 -0
- package/src/auth/codex-response.ts +78 -0
- package/src/auth/codex-shared.ts +3 -0
- package/src/auth/headers.ts +18 -0
- package/src/auth/index.ts +153 -0
- package/src/auth/oauth-fetch.ts +100 -0
- package/src/auth/refresh.ts +102 -0
- package/src/auth/retry.ts +70 -0
- package/src/auth/store.ts +45 -0
- package/src/auth/types.ts +27 -0
- package/src/auth.ts +1 -725
- package/src/constants/errors.ts +24 -0
- package/src/constants/guards.ts +34 -0
- package/src/constants/options.ts +37 -0
- package/src/constants/plugin.ts +10 -0
- package/src/constants/types.ts +147 -0
- package/src/constants.ts +5 -261
- package/src/labels.ts +0 -2
- package/src/translator/index.ts +125 -0
- package/src/translator/part-id.ts +43 -0
- package/src/translator/provider.ts +81 -0
- package/src/translator/retry.ts +62 -0
- package/src/translator/types.ts +17 -0
- package/src/translator.ts +1 -326
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { FetchLike } from "../constants"
|
|
2
|
+
|
|
3
|
+
const providerFactoryCache = new Map<string, unknown>()
|
|
4
|
+
|
|
5
|
+
export function __resetProviderFactoryCacheForTest() {
|
|
6
|
+
providerFactoryCache.clear()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function loadFactory(providerID: string): Promise<unknown> {
|
|
10
|
+
const cached = providerFactoryCache.get(providerID)
|
|
11
|
+
if (cached) return cached
|
|
12
|
+
|
|
13
|
+
let factory: unknown
|
|
14
|
+
if (providerID === "anthropic") {
|
|
15
|
+
const mod = await import("@ai-sdk/anthropic")
|
|
16
|
+
factory = mod.createAnthropic ?? mod.anthropic
|
|
17
|
+
} else if (providerID === "openai") {
|
|
18
|
+
const mod = await import("@ai-sdk/openai")
|
|
19
|
+
factory = mod.createOpenAI ?? mod.openai
|
|
20
|
+
} else if (providerID === "google") {
|
|
21
|
+
const mod = await import("@ai-sdk/google")
|
|
22
|
+
factory = mod.createGoogleGenerativeAI ?? mod.google
|
|
23
|
+
} else if (providerID === "google-vertex") {
|
|
24
|
+
const mod = await import("@ai-sdk/google-vertex")
|
|
25
|
+
factory = mod.createVertex ?? mod.vertex
|
|
26
|
+
} else if (providerID === "amazon-bedrock") {
|
|
27
|
+
const mod = await import("@ai-sdk/amazon-bedrock")
|
|
28
|
+
factory = mod.createAmazonBedrock ?? mod.bedrock
|
|
29
|
+
} else if (providerID === "github-copilot") {
|
|
30
|
+
const mod = await import("@ai-sdk/openai-compatible")
|
|
31
|
+
factory = mod.createOpenAICompatible
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error(`Unsupported translator provider "${providerID}"`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof factory !== "function") {
|
|
37
|
+
throw new Error(`Unable to load provider factory for "${providerID}"`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
providerFactoryCache.set(providerID, factory)
|
|
41
|
+
return factory
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function instantiateProvider(
|
|
45
|
+
factory: unknown,
|
|
46
|
+
providerID: string,
|
|
47
|
+
credentials: { apiKey?: string; fetch?: FetchLike },
|
|
48
|
+
): unknown {
|
|
49
|
+
if (typeof factory !== "function") throw new Error(`Invalid provider factory for "${providerID}"`)
|
|
50
|
+
|
|
51
|
+
const config = {
|
|
52
|
+
...(credentials.apiKey !== undefined ? { apiKey: credentials.apiKey } : {}),
|
|
53
|
+
...(credentials.fetch ? { fetch: credentials.fetch } : {}),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (providerID === "github-copilot") {
|
|
57
|
+
return (factory as (config: Record<string, unknown>) => unknown)({
|
|
58
|
+
...config,
|
|
59
|
+
name: "github-copilot",
|
|
60
|
+
baseURL: "https://api.githubcopilot.com",
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (factory as (config: Record<string, unknown>) => unknown)(config)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function instantiateModel(provider: unknown, modelID: string): unknown {
|
|
68
|
+
if (typeof provider === "function") return provider(modelID)
|
|
69
|
+
if (provider && typeof provider === "object") {
|
|
70
|
+
const record = provider as Record<string, unknown>
|
|
71
|
+
if (typeof record.chatModel === "function") return (record.chatModel as (id: string) => unknown)(modelID)
|
|
72
|
+
if (typeof record.languageModel === "function") return (record.languageModel as (id: string) => unknown)(modelID)
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Unable to instantiate model "${modelID}"`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function supportsTemperature(providerID: string, modelID: string): boolean {
|
|
78
|
+
if (providerID !== "openai") return true
|
|
79
|
+
if (modelID.startsWith("o1") || modelID.startsWith("o3") || modelID.startsWith("o4-mini")) return false
|
|
80
|
+
return !(modelID.startsWith("gpt-5") && !modelID.startsWith("gpt-5-chat"))
|
|
81
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { normalizeReason } from "../constants"
|
|
2
|
+
|
|
3
|
+
function getStatus(error: unknown): number | undefined {
|
|
4
|
+
if (!error || typeof error !== "object") return undefined
|
|
5
|
+
const record = error as Record<string, unknown>
|
|
6
|
+
if (typeof record.status === "number") return record.status
|
|
7
|
+
if (typeof record.statusCode === "number") return record.statusCode
|
|
8
|
+
const response = record.response
|
|
9
|
+
if (response && typeof response === "object") {
|
|
10
|
+
const status = (response as Record<string, unknown>).status
|
|
11
|
+
if (typeof status === "number") return status
|
|
12
|
+
}
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getRetryAfterMs(error: unknown): number {
|
|
17
|
+
if (!error || typeof error !== "object") return 2000
|
|
18
|
+
const response = (error as Record<string, unknown>).response
|
|
19
|
+
if (!response || typeof response !== "object") return 2000
|
|
20
|
+
const headers = (response as { headers?: Headers }).headers
|
|
21
|
+
if (!(headers instanceof Headers)) return 2000
|
|
22
|
+
const retryAfter = headers.get("retry-after")
|
|
23
|
+
if (!retryAfter) return 2000
|
|
24
|
+
const seconds = Number(retryAfter)
|
|
25
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000)
|
|
26
|
+
const date = Date.parse(retryAfter)
|
|
27
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 2000
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isRetryable(error: unknown): boolean {
|
|
31
|
+
const status = getStatus(error)
|
|
32
|
+
if (status === 429) return true
|
|
33
|
+
if (status !== undefined) return status >= 500
|
|
34
|
+
const message = normalizeReason(error).toLowerCase()
|
|
35
|
+
return (
|
|
36
|
+
message.includes("network") ||
|
|
37
|
+
message.includes("fetch") ||
|
|
38
|
+
message.includes("timeout") ||
|
|
39
|
+
message.includes("socket") ||
|
|
40
|
+
message.includes("econn")
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function withRetry<T>(task: () => Promise<T>, sleepImpl: (ms: number) => Promise<void>): Promise<T> {
|
|
45
|
+
let lastError: unknown
|
|
46
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
47
|
+
try {
|
|
48
|
+
return await task()
|
|
49
|
+
} catch (error) {
|
|
50
|
+
lastError = error
|
|
51
|
+
if (!isRetryable(error)) throw error
|
|
52
|
+
if (getStatus(error) === 429) {
|
|
53
|
+
if (attempt >= 1) throw error
|
|
54
|
+
await sleepImpl(getRetryAfterMs(error))
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
if (attempt >= 2) throw error
|
|
58
|
+
await sleepImpl(attempt === 0 ? 500 : 1500)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw lastError
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { generateText } from "ai"
|
|
2
|
+
import type { createCredentialResolver } from "../auth"
|
|
3
|
+
|
|
4
|
+
export interface TranslatorDependencies {
|
|
5
|
+
generateTextImpl?: typeof generateText
|
|
6
|
+
sleep?: (ms: number) => Promise<void>
|
|
7
|
+
now?: () => number
|
|
8
|
+
credentialResolver?: ReturnType<typeof createCredentialResolver>
|
|
9
|
+
timeoutMs?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TranslateTextInput {
|
|
13
|
+
text: string
|
|
14
|
+
sourceLanguage: string
|
|
15
|
+
targetLanguage: string
|
|
16
|
+
direction: "inbound" | "outbound"
|
|
17
|
+
}
|
package/src/translator.ts
CHANGED
|
@@ -1,326 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { setTimeout as sleep } from "node:timers/promises"
|
|
3
|
-
import { generateText } from "ai"
|
|
4
|
-
import { createCredentialResolver } from "./auth"
|
|
5
|
-
import {
|
|
6
|
-
buildAuthUnavailableError,
|
|
7
|
-
type FetchLike,
|
|
8
|
-
normalizeReason,
|
|
9
|
-
PLUGIN_NAME,
|
|
10
|
-
type PluginClientLike,
|
|
11
|
-
type ProviderInfo,
|
|
12
|
-
parseTranslatorModel,
|
|
13
|
-
type ResolvedTranslateOptions,
|
|
14
|
-
} from "./constants"
|
|
15
|
-
import { buildSystemPrompt, buildUserPrompt, unwrapEchoedTextEnvelope } from "./prompts"
|
|
16
|
-
|
|
17
|
-
interface TranslatorDependencies {
|
|
18
|
-
generateTextImpl?: typeof generateText
|
|
19
|
-
sleep?: (ms: number) => Promise<void>
|
|
20
|
-
now?: () => number
|
|
21
|
-
credentialResolver?: ReturnType<typeof createCredentialResolver>
|
|
22
|
-
timeoutMs?: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface TranslateTextInput {
|
|
26
|
-
text: string
|
|
27
|
-
sourceLanguage: string
|
|
28
|
-
targetLanguage: string
|
|
29
|
-
direction: "inbound" | "outbound"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Hard timeout for a single generateText call. Without this, a stalled
|
|
33
|
-
// provider request can block the chat.message hook indefinitely. Long
|
|
34
|
-
// assistant responses can require well over a minute to translate, so
|
|
35
|
-
// the budget here is generous; retries will still bound the worst case.
|
|
36
|
-
const DEFAULT_TRANSLATE_TIMEOUT_MS = 180_000
|
|
37
|
-
|
|
38
|
-
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
39
|
-
return new Promise<T>((resolve, reject) => {
|
|
40
|
-
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
41
|
-
promise.then(
|
|
42
|
-
(value) => {
|
|
43
|
-
clearTimeout(timer)
|
|
44
|
-
resolve(value)
|
|
45
|
-
},
|
|
46
|
-
(error) => {
|
|
47
|
-
clearTimeout(timer)
|
|
48
|
-
reject(error)
|
|
49
|
-
},
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const providerFactoryCache = new Map<string, unknown>()
|
|
55
|
-
|
|
56
|
-
const PART_ID_LENGTH = 26
|
|
57
|
-
const PART_ID_PREFIX = "prt"
|
|
58
|
-
const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
59
|
-
let partLastTimestamp = 0
|
|
60
|
-
let partCounter = 0
|
|
61
|
-
|
|
62
|
-
export function __resetTranslatorCachesForTest() {
|
|
63
|
-
providerFactoryCache.clear()
|
|
64
|
-
__resetSyntheticPartIDForTest()
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function __resetSyntheticPartIDForTest() {
|
|
68
|
-
partLastTimestamp = 0
|
|
69
|
-
partCounter = 0
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function randomBase62(length: number): string {
|
|
73
|
-
const bytes = randomBytes(length)
|
|
74
|
-
let result = ""
|
|
75
|
-
for (let index = 0; index < length; index += 1) {
|
|
76
|
-
result += BASE62_CHARS[bytes[index] % BASE62_CHARS.length]
|
|
77
|
-
}
|
|
78
|
-
return result
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function getStatus(error: unknown): number | undefined {
|
|
82
|
-
if (!error || typeof error !== "object") return undefined
|
|
83
|
-
const record = error as Record<string, unknown>
|
|
84
|
-
if (typeof record.status === "number") return record.status
|
|
85
|
-
if (typeof record.statusCode === "number") return record.statusCode
|
|
86
|
-
const response = record.response
|
|
87
|
-
if (response && typeof response === "object") {
|
|
88
|
-
const status = (response as Record<string, unknown>).status
|
|
89
|
-
if (typeof status === "number") return status
|
|
90
|
-
}
|
|
91
|
-
return undefined
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getRetryAfterMs(error: unknown): number {
|
|
95
|
-
if (!error || typeof error !== "object") return 2000
|
|
96
|
-
const record = error as Record<string, unknown>
|
|
97
|
-
const response = record.response
|
|
98
|
-
if (!response || typeof response !== "object") return 2000
|
|
99
|
-
const headers = (response as { headers?: Headers }).headers
|
|
100
|
-
if (!(headers instanceof Headers)) return 2000
|
|
101
|
-
const retryAfter = headers.get("retry-after")
|
|
102
|
-
if (!retryAfter) return 2000
|
|
103
|
-
const seconds = Number(retryAfter)
|
|
104
|
-
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000)
|
|
105
|
-
const date = Date.parse(retryAfter)
|
|
106
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 2000
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function isRetryable(error: unknown): boolean {
|
|
110
|
-
const status = getStatus(error)
|
|
111
|
-
if (status === 429) return true
|
|
112
|
-
if (status !== undefined) return status >= 500
|
|
113
|
-
const message = normalizeReason(error).toLowerCase()
|
|
114
|
-
return (
|
|
115
|
-
message.includes("network") ||
|
|
116
|
-
message.includes("fetch") ||
|
|
117
|
-
message.includes("timeout") ||
|
|
118
|
-
message.includes("socket") ||
|
|
119
|
-
message.includes("econn")
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function withRetry<T>(task: () => Promise<T>, sleepImpl: (ms: number) => Promise<void>): Promise<T> {
|
|
124
|
-
let lastError: unknown
|
|
125
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
126
|
-
try {
|
|
127
|
-
return await task()
|
|
128
|
-
} catch (error) {
|
|
129
|
-
lastError = error
|
|
130
|
-
if (!isRetryable(error)) throw error
|
|
131
|
-
if (getStatus(error) === 429) {
|
|
132
|
-
if (attempt >= 1) throw error
|
|
133
|
-
await sleepImpl(getRetryAfterMs(error))
|
|
134
|
-
continue
|
|
135
|
-
}
|
|
136
|
-
if (attempt >= 2) throw error
|
|
137
|
-
await sleepImpl(attempt === 0 ? 500 : 1500)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
throw lastError
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function loadFactory(providerID: string): Promise<unknown> {
|
|
144
|
-
const cached = providerFactoryCache.get(providerID)
|
|
145
|
-
if (cached) return cached
|
|
146
|
-
|
|
147
|
-
let factory: unknown
|
|
148
|
-
if (providerID === "anthropic") {
|
|
149
|
-
const mod = await import("@ai-sdk/anthropic")
|
|
150
|
-
factory = mod.createAnthropic ?? mod.anthropic
|
|
151
|
-
} else if (providerID === "openai") {
|
|
152
|
-
const mod = await import("@ai-sdk/openai")
|
|
153
|
-
factory = mod.createOpenAI ?? mod.openai
|
|
154
|
-
} else if (providerID === "google") {
|
|
155
|
-
const mod = await import("@ai-sdk/google")
|
|
156
|
-
factory = mod.createGoogleGenerativeAI ?? mod.google
|
|
157
|
-
} else if (providerID === "google-vertex") {
|
|
158
|
-
const mod = await import("@ai-sdk/google-vertex")
|
|
159
|
-
factory = mod.createVertex ?? mod.vertex
|
|
160
|
-
} else if (providerID === "amazon-bedrock") {
|
|
161
|
-
const mod = await import("@ai-sdk/amazon-bedrock")
|
|
162
|
-
factory = mod.createAmazonBedrock ?? mod.bedrock
|
|
163
|
-
} else if (providerID === "github-copilot") {
|
|
164
|
-
const mod = await import("@ai-sdk/openai-compatible")
|
|
165
|
-
factory = mod.createOpenAICompatible
|
|
166
|
-
} else {
|
|
167
|
-
throw new Error(`Unsupported translator provider "${providerID}"`)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (typeof factory !== "function") {
|
|
171
|
-
throw new Error(`Unable to load provider factory for "${providerID}"`)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
providerFactoryCache.set(providerID, factory)
|
|
175
|
-
return factory
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function instantiateProvider(
|
|
179
|
-
factory: unknown,
|
|
180
|
-
providerID: string,
|
|
181
|
-
credentials: { apiKey?: string; fetch?: FetchLike },
|
|
182
|
-
): unknown {
|
|
183
|
-
if (typeof factory !== "function") throw new Error(`Invalid provider factory for "${providerID}"`)
|
|
184
|
-
|
|
185
|
-
const config = {
|
|
186
|
-
...(credentials.apiKey !== undefined ? { apiKey: credentials.apiKey } : {}),
|
|
187
|
-
...(credentials.fetch ? { fetch: credentials.fetch } : {}),
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (providerID === "github-copilot") {
|
|
191
|
-
return (factory as (config: Record<string, unknown>) => unknown)({
|
|
192
|
-
...config,
|
|
193
|
-
name: "github-copilot",
|
|
194
|
-
baseURL: "https://api.githubcopilot.com",
|
|
195
|
-
})
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return (factory as (config: Record<string, unknown>) => unknown)(config)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function instantiateModel(provider: unknown, modelID: string): unknown {
|
|
202
|
-
if (typeof provider === "function") return provider(modelID)
|
|
203
|
-
if (provider && typeof provider === "object") {
|
|
204
|
-
const record = provider as Record<string, unknown>
|
|
205
|
-
if (typeof record.chatModel === "function") return (record.chatModel as (id: string) => unknown)(modelID)
|
|
206
|
-
if (typeof record.languageModel === "function") {
|
|
207
|
-
return (record.languageModel as (id: string) => unknown)(modelID)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
throw new Error(`Unable to instantiate model "${modelID}"`)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function supportsTemperature(providerID: string, modelID: string): boolean {
|
|
214
|
-
if (providerID !== "openai") return true
|
|
215
|
-
if (modelID.startsWith("o1") || modelID.startsWith("o3") || modelID.startsWith("o4-mini")) return false
|
|
216
|
-
return !(modelID.startsWith("gpt-5") && !modelID.startsWith("gpt-5-chat"))
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function isAuthMessage(error: unknown): boolean {
|
|
220
|
-
if (!(error instanceof Error)) return false
|
|
221
|
-
return error.message.includes(":AUTH_UNAVAILABLE]") || error.message.includes(":OAUTH_REFRESH_FAILED]")
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function modelProviderHint(providerID: string, provider?: ProviderInfo): Error {
|
|
225
|
-
return buildAuthUnavailableError(providerID, provider?.env[0] || "the provider's API key env var")
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function hashText(text: string): string {
|
|
229
|
-
return createHash("sha256").update(text, "utf8").digest("hex").slice(0, 16)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function createSyntheticPartID(): string {
|
|
233
|
-
const currentTimestamp = Date.now()
|
|
234
|
-
if (currentTimestamp !== partLastTimestamp) {
|
|
235
|
-
partLastTimestamp = currentTimestamp
|
|
236
|
-
partCounter = 0
|
|
237
|
-
}
|
|
238
|
-
partCounter += 1
|
|
239
|
-
|
|
240
|
-
const encoded = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(partCounter)
|
|
241
|
-
const timeBytes = Buffer.alloc(6)
|
|
242
|
-
for (let index = 0; index < 6; index += 1) {
|
|
243
|
-
timeBytes[index] = Number((encoded >> BigInt(40 - 8 * index)) & BigInt(0xff))
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return `${PART_ID_PREFIX}_${timeBytes.toString("hex")}${randomBase62(PART_ID_LENGTH - 12)}`
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function createTranslator(
|
|
250
|
-
client: PluginClientLike,
|
|
251
|
-
options: ResolvedTranslateOptions,
|
|
252
|
-
deps: TranslatorDependencies = {},
|
|
253
|
-
) {
|
|
254
|
-
const sleepImpl = deps.sleep ?? ((ms: number) => sleep(ms))
|
|
255
|
-
const now = deps.now ?? (() => Date.now())
|
|
256
|
-
const generateTextImpl = deps.generateTextImpl ?? generateText
|
|
257
|
-
const credentialResolver = deps.credentialResolver ?? createCredentialResolver(client, options)
|
|
258
|
-
const timeoutMs = deps.timeoutMs ?? DEFAULT_TRANSLATE_TIMEOUT_MS
|
|
259
|
-
|
|
260
|
-
async function translateText(input: TranslateTextInput): Promise<string> {
|
|
261
|
-
if (!input.text) return input.text
|
|
262
|
-
if (input.sourceLanguage === input.targetLanguage) return input.text
|
|
263
|
-
|
|
264
|
-
const startedAt = now()
|
|
265
|
-
const { providerID, modelID } = parseTranslatorModel(options.translatorModel)
|
|
266
|
-
const credentials = await credentialResolver.resolve(options.translatorModel)
|
|
267
|
-
const factory = await loadFactory(providerID)
|
|
268
|
-
const provider = instantiateProvider(factory, providerID, credentials)
|
|
269
|
-
const model = instantiateModel(provider, modelID)
|
|
270
|
-
|
|
271
|
-
const rawTranslated = await withRetry(async () => {
|
|
272
|
-
try {
|
|
273
|
-
const result = (await withTimeout(
|
|
274
|
-
generateTextImpl({
|
|
275
|
-
model: model as never,
|
|
276
|
-
system: buildSystemPrompt({
|
|
277
|
-
sourceLanguage: input.sourceLanguage,
|
|
278
|
-
targetLanguage: input.targetLanguage,
|
|
279
|
-
text: input.text,
|
|
280
|
-
}),
|
|
281
|
-
...(supportsTemperature(providerID, modelID) ? { temperature: 0 } : {}),
|
|
282
|
-
prompt: buildUserPrompt({
|
|
283
|
-
sourceLanguage: input.sourceLanguage,
|
|
284
|
-
targetLanguage: input.targetLanguage,
|
|
285
|
-
text: input.text,
|
|
286
|
-
}),
|
|
287
|
-
}) as Promise<{ text: string }>,
|
|
288
|
-
timeoutMs,
|
|
289
|
-
"Translator generateText",
|
|
290
|
-
)) as { text: string }
|
|
291
|
-
return result.text
|
|
292
|
-
} catch (error) {
|
|
293
|
-
if (isAuthMessage(error)) throw error
|
|
294
|
-
if (credentials.mode === "default" && credentialResolver.isMissingCredentialError(error)) {
|
|
295
|
-
throw modelProviderHint(providerID, credentials.provider)
|
|
296
|
-
}
|
|
297
|
-
throw error
|
|
298
|
-
}
|
|
299
|
-
}, sleepImpl)
|
|
300
|
-
const translated = unwrapEchoedTextEnvelope(rawTranslated)
|
|
301
|
-
|
|
302
|
-
if (options.verbose) {
|
|
303
|
-
await client.app.log({
|
|
304
|
-
body: {
|
|
305
|
-
service: PLUGIN_NAME,
|
|
306
|
-
level: "info",
|
|
307
|
-
message: "translated",
|
|
308
|
-
extra: {
|
|
309
|
-
direction: input.direction,
|
|
310
|
-
chars_in: input.text.length,
|
|
311
|
-
chars_out: translated.length,
|
|
312
|
-
ms: now() - startedAt,
|
|
313
|
-
cached: false,
|
|
314
|
-
model: options.translatorModel,
|
|
315
|
-
},
|
|
316
|
-
},
|
|
317
|
-
})
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return translated
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
translateText,
|
|
325
|
-
}
|
|
326
|
-
}
|
|
1
|
+
export * from "./translator/index"
|