opencode-translate 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/auth.ts +125 -19
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ If you prefer a plain API key, set `ANTHROPIC_API_KEY` in the environment or pas
|
|
|
127
127
|
|
|
128
128
|
If `translatorModel` uses OpenAI and OpenCode auth is backed by the ChatGPT/Codex OAuth flow, the plugin reuses those OAuth credentials for translation requests.
|
|
129
129
|
|
|
130
|
-
For OAuth-backed OpenAI requests, the plugin routes the OpenAI AI SDK request to `https://chatgpt.com/backend-api/codex/responses
|
|
130
|
+
For OAuth-backed OpenAI requests, the plugin routes the OpenAI AI SDK request to `https://chatgpt.com/backend-api/codex/responses`, adds the required Codex beta/originator headers, normalizes the request body to Codex's expected typed `input` shape, and converts Codex SSE responses back to JSON for translation calls. This supports models such as `openai/gpt-5.5` when your ChatGPT plan has access.
|
|
131
131
|
|
|
132
132
|
If you prefer a plain API key, set `OPENAI_API_KEY` in the environment or pass `apiKey` in plugin options. The plugin prefers explicit `apiKey`, then `OPENAI_API_KEY`, then OAuth.
|
|
133
133
|
|
package/package.json
CHANGED
package/src/auth.ts
CHANGED
|
@@ -142,24 +142,30 @@ function normalizeCodexInputItem(item: unknown, instructions: string[]): unknown
|
|
|
142
142
|
return item
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
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 }
|
|
147
152
|
|
|
148
153
|
let parsed: unknown
|
|
149
154
|
try {
|
|
150
155
|
parsed = JSON.parse(body)
|
|
151
156
|
} catch {
|
|
152
|
-
return body
|
|
157
|
+
return { body, originalStream: false }
|
|
153
158
|
}
|
|
154
159
|
|
|
155
|
-
if (!isRecord(parsed)) return body
|
|
160
|
+
if (!isRecord(parsed)) return { body, originalStream: false }
|
|
161
|
+
const originalStream = parsed.stream === true
|
|
156
162
|
|
|
157
163
|
const sourceInput = Array.isArray(parsed.input)
|
|
158
164
|
? parsed.input
|
|
159
165
|
: Array.isArray(parsed.messages)
|
|
160
166
|
? parsed.messages
|
|
161
167
|
: undefined
|
|
162
|
-
if (!sourceInput) return body
|
|
168
|
+
if (!sourceInput) return { body, originalStream }
|
|
163
169
|
|
|
164
170
|
const instructions: string[] = []
|
|
165
171
|
if (typeof parsed.instructions === "string" && parsed.instructions) instructions.push(parsed.instructions)
|
|
@@ -168,18 +174,110 @@ function rewriteOpenAICodexBody(body: BodyInit | null | undefined): BodyInit | n
|
|
|
168
174
|
.map((item) => normalizeCodexInputItem(item, instructions))
|
|
169
175
|
.filter((item): item is unknown => item !== undefined)
|
|
170
176
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 })
|
|
183
281
|
}
|
|
184
282
|
|
|
185
283
|
function isMissingCredentialError(error: unknown): boolean {
|
|
@@ -495,6 +593,7 @@ export function createCredentialResolver(
|
|
|
495
593
|
input instanceof URL ? new URL(input.href) : new URL(typeof input === "string" ? input : input.url)
|
|
496
594
|
|
|
497
595
|
let nextBody = init?.body
|
|
596
|
+
let convertCodexResponse = false
|
|
498
597
|
|
|
499
598
|
if (providerID === "anthropic") {
|
|
500
599
|
// Match the Claude Code CLI fingerprint so Anthropic's OAuth rate-limit
|
|
@@ -514,11 +613,16 @@ export function createCredentialResolver(
|
|
|
514
613
|
inputUrl.hostname === "api.openai.com" &&
|
|
515
614
|
(inputUrl.pathname === "/v1/chat/completions" || inputUrl.pathname === "/v1/responses")
|
|
516
615
|
) {
|
|
616
|
+
const rewritten = rewriteOpenAICodexBody(nextBody)
|
|
517
617
|
inputUrl.protocol = "https:"
|
|
518
618
|
inputUrl.hostname = "chatgpt.com"
|
|
519
619
|
inputUrl.pathname = "/backend-api/codex/responses"
|
|
520
620
|
inputUrl.search = ""
|
|
521
|
-
nextBody =
|
|
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")
|
|
522
626
|
headers.delete("content-length")
|
|
523
627
|
}
|
|
524
628
|
}
|
|
@@ -547,11 +651,13 @@ export function createCredentialResolver(
|
|
|
547
651
|
}
|
|
548
652
|
}
|
|
549
653
|
|
|
550
|
-
|
|
654
|
+
const response = await fetchImpl(inputUrl, {
|
|
551
655
|
...init,
|
|
552
656
|
headers,
|
|
553
657
|
body: nextBody,
|
|
554
658
|
})
|
|
659
|
+
if (convertCodexResponse && response.ok) return convertCodexSSEToJSON(response)
|
|
660
|
+
return response
|
|
555
661
|
}
|
|
556
662
|
}
|
|
557
663
|
|