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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. 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` and normalizes the request body to Codex's expected typed `input` shape. This supports models such as `openai/gpt-5.5` when your ChatGPT plan has access.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-translate",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "OpenCode plugin that lets the user chat in a configured source language while the main chat loop only sees English.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
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
- function rewriteOpenAICodexBody(body: BodyInit | null | undefined): BodyInit | null | undefined {
146
- if (typeof body !== "string") return body
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
- return JSON.stringify({
172
- ...parsed,
173
- instructions: instructions.join("\n\n"),
174
- input,
175
- tools: Array.isArray(parsed.tools) ? parsed.tools : [],
176
- tool_choice: typeof parsed.tool_choice === "string" ? parsed.tool_choice : "auto",
177
- parallel_tool_calls: typeof parsed.parallel_tool_calls === "boolean" ? parsed.parallel_tool_calls : false,
178
- store: typeof parsed.store === "boolean" ? parsed.store : false,
179
- stream: typeof parsed.stream === "boolean" ? parsed.stream : false,
180
- include: Array.isArray(parsed.include) ? parsed.include : [],
181
- messages: undefined,
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 = rewriteOpenAICodexBody(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
- return fetchImpl(inputUrl, {
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