opencode-translate 0.0.15 → 0.0.16
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 +68 -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,53 @@ 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 parseCodexSSEResponse(text: string): unknown | undefined {
|
|
202
|
+
for (const line of text.split(/\r?\n/)) {
|
|
203
|
+
if (!line.startsWith("data: ")) continue
|
|
204
|
+
const payload = line.slice(6).trim()
|
|
205
|
+
if (!payload || payload === "[DONE]") continue
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(payload) as Record<string, unknown>
|
|
208
|
+
if ((parsed.type === "response.done" || parsed.type === "response.completed") && parsed.response) {
|
|
209
|
+
return parsed.response
|
|
210
|
+
}
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
return undefined
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function convertCodexSSEToJSON(response: Response): Promise<Response> {
|
|
217
|
+
const headers = new Headers(response.headers)
|
|
218
|
+
const text = await response.text()
|
|
219
|
+
const parsed = parseCodexSSEResponse(text)
|
|
220
|
+
if (!parsed) return new Response(text, { status: response.status, statusText: response.statusText, headers })
|
|
221
|
+
|
|
222
|
+
headers.set("content-type", "application/json; charset=utf-8")
|
|
223
|
+
return new Response(JSON.stringify(parsed), { status: response.status, statusText: response.statusText, headers })
|
|
183
224
|
}
|
|
184
225
|
|
|
185
226
|
function isMissingCredentialError(error: unknown): boolean {
|
|
@@ -495,6 +536,7 @@ export function createCredentialResolver(
|
|
|
495
536
|
input instanceof URL ? new URL(input.href) : new URL(typeof input === "string" ? input : input.url)
|
|
496
537
|
|
|
497
538
|
let nextBody = init?.body
|
|
539
|
+
let convertCodexResponse = false
|
|
498
540
|
|
|
499
541
|
if (providerID === "anthropic") {
|
|
500
542
|
// Match the Claude Code CLI fingerprint so Anthropic's OAuth rate-limit
|
|
@@ -514,11 +556,16 @@ export function createCredentialResolver(
|
|
|
514
556
|
inputUrl.hostname === "api.openai.com" &&
|
|
515
557
|
(inputUrl.pathname === "/v1/chat/completions" || inputUrl.pathname === "/v1/responses")
|
|
516
558
|
) {
|
|
559
|
+
const rewritten = rewriteOpenAICodexBody(nextBody)
|
|
517
560
|
inputUrl.protocol = "https:"
|
|
518
561
|
inputUrl.hostname = "chatgpt.com"
|
|
519
562
|
inputUrl.pathname = "/backend-api/codex/responses"
|
|
520
563
|
inputUrl.search = ""
|
|
521
|
-
nextBody =
|
|
564
|
+
nextBody = rewritten.body
|
|
565
|
+
convertCodexResponse = !rewritten.originalStream
|
|
566
|
+
headers.set("OpenAI-Beta", "responses=experimental")
|
|
567
|
+
headers.set("originator", "codex_cli_rs")
|
|
568
|
+
headers.set("accept", "text/event-stream")
|
|
522
569
|
headers.delete("content-length")
|
|
523
570
|
}
|
|
524
571
|
}
|
|
@@ -547,11 +594,13 @@ export function createCredentialResolver(
|
|
|
547
594
|
}
|
|
548
595
|
}
|
|
549
596
|
|
|
550
|
-
|
|
597
|
+
const response = await fetchImpl(inputUrl, {
|
|
551
598
|
...init,
|
|
552
599
|
headers,
|
|
553
600
|
body: nextBody,
|
|
554
601
|
})
|
|
602
|
+
if (convertCodexResponse && response.ok) return convertCodexSSEToJSON(response)
|
|
603
|
+
return response
|
|
555
604
|
}
|
|
556
605
|
}
|
|
557
606
|
|