opencode-translate 0.0.14 → 0.0.15

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 CHANGED
@@ -123,6 +123,14 @@ Tradeoffs:
123
123
 
124
124
  If you prefer a plain API key, set `ANTHROPIC_API_KEY` in the environment or pass `apiKey` in plugin options. The plugin prefers explicit `apiKey`, then `ANTHROPIC_API_KEY`, then OAuth.
125
125
 
126
+ ## OpenAI OAuth Support
127
+
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
+
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.
131
+
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
+
126
134
  ## Manual Smoke Test
127
135
 
128
136
  1. Install the plugin and configure `sourceLanguage: "ko"`, `displayLanguage: "ko"`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-translate",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
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
@@ -78,6 +78,110 @@ function setUserAgent(headers: Headers, packageVersion?: string) {
78
78
  headers.set("User-Agent", packageVersion ? `${USER_AGENT.replace("0.0.0", packageVersion)}` : USER_AGENT)
79
79
  }
80
80
 
81
+ function isRecord(value: unknown): value is Record<string, unknown> {
82
+ return !!value && typeof value === "object" && !Array.isArray(value)
83
+ }
84
+
85
+ function textFromContent(content: unknown): string | undefined {
86
+ if (typeof content === "string") return content
87
+ if (!Array.isArray(content)) return undefined
88
+
89
+ const text = content
90
+ .map((part) => (isRecord(part) && typeof part.text === "string" ? part.text : undefined))
91
+ .filter((value): value is string => value !== undefined)
92
+ .join("\n")
93
+
94
+ return text || undefined
95
+ }
96
+
97
+ function normalizeCodexContent(role: string, content: unknown): Record<string, unknown>[] {
98
+ const textType = role === "assistant" ? "output_text" : "input_text"
99
+ if (typeof content === "string") return [{ type: textType, text: content }]
100
+ if (!Array.isArray(content)) return []
101
+
102
+ const result: Record<string, unknown>[] = []
103
+ for (const part of content) {
104
+ if (!isRecord(part)) continue
105
+ const type = part.type
106
+ if (type === "input_text" || type === "output_text") {
107
+ result.push({ ...part, type: textType })
108
+ continue
109
+ }
110
+ if (type === "input_image") {
111
+ result.push({ ...part })
112
+ continue
113
+ }
114
+ if (typeof part.text === "string") {
115
+ result.push({ type: textType, text: part.text })
116
+ }
117
+ }
118
+
119
+ return result
120
+ }
121
+
122
+ function normalizeCodexInputItem(item: unknown, instructions: string[]): unknown | undefined {
123
+ if (!isRecord(item)) return item
124
+ const role = typeof item.role === "string" ? item.role : undefined
125
+
126
+ if (role === "system" || role === "developer") {
127
+ const text = textFromContent(item.content)
128
+ if (text) instructions.push(text)
129
+ return undefined
130
+ }
131
+
132
+ if (item.type === "message" && role) {
133
+ const content = normalizeCodexContent(role, item.content)
134
+ return content.length > 0 ? { ...item, role, content } : undefined
135
+ }
136
+
137
+ if (role) {
138
+ const content = normalizeCodexContent(role, item.content)
139
+ return content.length > 0 ? { type: "message", role, content } : undefined
140
+ }
141
+
142
+ return item
143
+ }
144
+
145
+ function rewriteOpenAICodexBody(body: BodyInit | null | undefined): BodyInit | null | undefined {
146
+ if (typeof body !== "string") return body
147
+
148
+ let parsed: unknown
149
+ try {
150
+ parsed = JSON.parse(body)
151
+ } catch {
152
+ return body
153
+ }
154
+
155
+ if (!isRecord(parsed)) return body
156
+
157
+ const sourceInput = Array.isArray(parsed.input)
158
+ ? parsed.input
159
+ : Array.isArray(parsed.messages)
160
+ ? parsed.messages
161
+ : undefined
162
+ if (!sourceInput) return body
163
+
164
+ const instructions: string[] = []
165
+ if (typeof parsed.instructions === "string" && parsed.instructions) instructions.push(parsed.instructions)
166
+
167
+ const input = sourceInput
168
+ .map((item) => normalizeCodexInputItem(item, instructions))
169
+ .filter((item): item is unknown => item !== undefined)
170
+
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
+ })
183
+ }
184
+
81
185
  function isMissingCredentialError(error: unknown): boolean {
82
186
  const message = normalizeReason(error).toLowerCase()
83
187
  return (
@@ -244,7 +348,6 @@ async function refreshOpenAI(
244
348
  grant_type: "refresh_token",
245
349
  refresh_token: info.refresh,
246
350
  client_id: "app_EMoamEEZ73f0CkXaXp7hrann",
247
- scope: "openid profile email offline_access",
248
351
  })
249
352
  const response = await withRetry(
250
353
  () =>
@@ -415,6 +518,8 @@ export function createCredentialResolver(
415
518
  inputUrl.hostname = "chatgpt.com"
416
519
  inputUrl.pathname = "/backend-api/codex/responses"
417
520
  inputUrl.search = ""
521
+ nextBody = rewriteOpenAICodexBody(nextBody)
522
+ headers.delete("content-length")
418
523
  }
419
524
  }
420
525
 
package/src/translator.ts CHANGED
@@ -210,6 +210,12 @@ function instantiateModel(provider: unknown, modelID: string): unknown {
210
210
  throw new Error(`Unable to instantiate model "${modelID}"`)
211
211
  }
212
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
+
213
219
  function isAuthMessage(error: unknown): boolean {
214
220
  if (!(error instanceof Error)) return false
215
221
  return error.message.includes(":AUTH_UNAVAILABLE]") || error.message.includes(":OAUTH_REFRESH_FAILED]")
@@ -272,7 +278,7 @@ export function createTranslator(
272
278
  targetLanguage: input.targetLanguage,
273
279
  text: input.text,
274
280
  }),
275
- temperature: 0,
281
+ ...(supportsTemperature(providerID, modelID) ? { temperature: 0 } : {}),
276
282
  prompt: buildUserPrompt({
277
283
  sourceLanguage: input.sourceLanguage,
278
284
  targetLanguage: input.targetLanguage,