opencode-translate 1.0.6 → 1.0.7
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/dist/index.js +2271 -0
- package/index.d.ts +5 -0
- package/package.json +10 -4
- package/src/activation/chat-message.ts +0 -189
- package/src/activation/index.ts +0 -38
- package/src/activation/logging.ts +0 -11
- package/src/activation/messages-transform.ts +0 -44
- package/src/activation/metadata.ts +0 -41
- package/src/activation/parts.ts +0 -46
- package/src/activation/question-hooks.ts +0 -126
- package/src/activation/state.ts +0 -97
- package/src/activation/text-complete.ts +0 -50
- package/src/activation/trigger.ts +0 -57
- package/src/activation/types.ts +0 -47
- package/src/activation.ts +0 -1
- package/src/anthropic-oauth.ts +0 -148
- package/src/auth/codex-request.ts +0 -108
- package/src/auth/codex-response.ts +0 -78
- package/src/auth/codex-shared.ts +0 -3
- package/src/auth/headers.ts +0 -18
- package/src/auth/index.ts +0 -177
- package/src/auth/oauth-fetch.ts +0 -100
- package/src/auth/refresh.ts +0 -102
- package/src/auth/retry.ts +0 -70
- package/src/auth/store.ts +0 -98
- package/src/auth/types.ts +0 -27
- package/src/auth.ts +0 -1
- package/src/constants/errors.ts +0 -24
- package/src/constants/guards.ts +0 -33
- package/src/constants/options.ts +0 -55
- package/src/constants/plugin.ts +0 -9
- package/src/constants/types.ts +0 -159
- package/src/constants.ts +0 -5
- package/src/formatting.ts +0 -157
- package/src/index.ts +0 -7
- package/src/labels.ts +0 -3
- package/src/prompts.ts +0 -123
- package/src/question-tool.ts +0 -234
- package/src/translator/index.ts +0 -172
- package/src/translator/part-id.ts +0 -43
- package/src/translator/provider.ts +0 -411
- package/src/translator/retry.ts +0 -62
- package/src/translator/types.ts +0 -24
- package/src/translator.ts +0 -1
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { Hooks } from "@opencode-ai/plugin"
|
|
2
|
-
import { LLM_LANGUAGE, type MessageWithPartsLike, unwrapData } from "../constants"
|
|
3
|
-
import { composeTranslatedAssistantText, composeTranslationFailureText } from "../formatting"
|
|
4
|
-
import { getDisplayLanguageLabel } from "../labels"
|
|
5
|
-
import { logError } from "./logging"
|
|
6
|
-
import { resolveSessionState } from "./state"
|
|
7
|
-
import type { HookContext } from "./types"
|
|
8
|
-
|
|
9
|
-
type TextCompleteHook = NonNullable<Hooks["experimental.text.complete"]>
|
|
10
|
-
|
|
11
|
-
export function createTextCompleteHook(ctx: HookContext): TextCompleteHook {
|
|
12
|
-
return async (input, output) => {
|
|
13
|
-
try {
|
|
14
|
-
const resolved = await resolveSessionState(ctx.client, ctx.directory, input.sessionID)
|
|
15
|
-
const activeState = resolved.state
|
|
16
|
-
if (!activeState) return
|
|
17
|
-
|
|
18
|
-
const message = unwrapData(
|
|
19
|
-
await ctx.client.session.message({
|
|
20
|
-
path: { id: input.sessionID, messageID: input.messageID },
|
|
21
|
-
query: { ...(ctx.directory ? { directory: ctx.directory } : {}) },
|
|
22
|
-
throwOnError: true,
|
|
23
|
-
}),
|
|
24
|
-
) as MessageWithPartsLike & { info: Record<string, unknown> }
|
|
25
|
-
|
|
26
|
-
if (message.info.role !== "assistant") return
|
|
27
|
-
if (message.info.summary === true) return
|
|
28
|
-
if (activeState.translate_user_lang === LLM_LANGUAGE || output.text.length === 0) return
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const translated = await ctx.translator.translateText({
|
|
32
|
-
text: output.text,
|
|
33
|
-
sourceLanguage: LLM_LANGUAGE,
|
|
34
|
-
targetLanguage: activeState.translate_user_lang,
|
|
35
|
-
direction: "outbound",
|
|
36
|
-
})
|
|
37
|
-
output.text = composeTranslatedAssistantText(
|
|
38
|
-
output.text,
|
|
39
|
-
getDisplayLanguageLabel(activeState.translate_user_lang),
|
|
40
|
-
translated,
|
|
41
|
-
)
|
|
42
|
-
} catch (error) {
|
|
43
|
-
output.text = composeTranslationFailureText(output.text)
|
|
44
|
-
await logError(ctx.client, error)
|
|
45
|
-
}
|
|
46
|
-
} catch (error) {
|
|
47
|
-
await logError(ctx.client, error)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { isUserAuthoredTextPart, type TextPartLike } from "../constants"
|
|
2
|
-
import type { TriggerMatch } from "./types"
|
|
3
|
-
|
|
4
|
-
function escapeRegex(value: string): string {
|
|
5
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function findTriggerMatch(parts: TextPartLike[], trigger: string[]): TriggerMatch | undefined {
|
|
9
|
-
let eligibleIndex = 0
|
|
10
|
-
for (let partArrayIndex = 0; partArrayIndex < parts.length; partArrayIndex += 1) {
|
|
11
|
-
const part = parts[partArrayIndex]
|
|
12
|
-
if (!isUserAuthoredTextPart(part)) continue
|
|
13
|
-
|
|
14
|
-
let bestForPart: TriggerMatch | undefined
|
|
15
|
-
for (const keyword of trigger) {
|
|
16
|
-
const pattern = new RegExp(`(^|[ \\t\\r\\n\\f\\v])${escapeRegex(keyword)}(?=$|[ \\t\\r\\n\\f\\v])`)
|
|
17
|
-
const match = pattern.exec(part.text)
|
|
18
|
-
if (!match) continue
|
|
19
|
-
const offset = match.index + match[1].length
|
|
20
|
-
if (!bestForPart || offset < bestForPart.offset) bestForPart = { partArrayIndex, eligibleIndex, keyword, offset }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (bestForPart) return bestForPart
|
|
24
|
-
eligibleIndex += 1
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return undefined
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function stripTriggerKeyword(text: string, keyword: string, offset: number): string {
|
|
31
|
-
const lineStart = text.lastIndexOf("\n", offset - 1) + 1
|
|
32
|
-
const nextNewline = text.indexOf("\n", offset)
|
|
33
|
-
const lineEnd = nextNewline === -1 ? text.length : nextNewline
|
|
34
|
-
const line = text.slice(lineStart, lineEnd)
|
|
35
|
-
const localOffset = offset - lineStart
|
|
36
|
-
|
|
37
|
-
let rewrittenLine: string
|
|
38
|
-
if (localOffset === 0 && line.startsWith(`${keyword} `)) {
|
|
39
|
-
rewrittenLine = line.slice(keyword.length + 1)
|
|
40
|
-
} else if (
|
|
41
|
-
localOffset + keyword.length === line.length &&
|
|
42
|
-
localOffset > 0 &&
|
|
43
|
-
line.slice(localOffset - 1, localOffset) === " "
|
|
44
|
-
) {
|
|
45
|
-
rewrittenLine = line.slice(0, localOffset - 1)
|
|
46
|
-
} else if (
|
|
47
|
-
localOffset > 0 &&
|
|
48
|
-
line.slice(localOffset - 1, localOffset) === " " &&
|
|
49
|
-
line.slice(localOffset + keyword.length, localOffset + keyword.length + 1) === " "
|
|
50
|
-
) {
|
|
51
|
-
rewrittenLine = `${line.slice(0, localOffset - 1)} ${line.slice(localOffset + keyword.length + 1)}`
|
|
52
|
-
} else {
|
|
53
|
-
rewrittenLine = `${line.slice(0, localOffset)}${line.slice(localOffset + keyword.length)}`
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return `${text.slice(0, lineStart)}${rewrittenLine}${text.slice(lineEnd)}`
|
|
57
|
-
}
|
package/src/activation/types.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { PluginClientLike, ResolvedTranslateOptions, TranslateState } from "../constants"
|
|
2
|
-
|
|
3
|
-
export const INACTIVE_ROOT_SESSION = "inactive-root"
|
|
4
|
-
export const INACTIVE_CHILD_SESSION = "inactive-child"
|
|
5
|
-
export const QUESTION_TOOL_ID = "question"
|
|
6
|
-
|
|
7
|
-
export type CachedSessionState = TranslateState | typeof INACTIVE_ROOT_SESSION | typeof INACTIVE_CHILD_SESSION
|
|
8
|
-
|
|
9
|
-
export interface ResolvedSessionState {
|
|
10
|
-
sessionActive: boolean
|
|
11
|
-
canActivate: boolean
|
|
12
|
-
state?: TranslateState
|
|
13
|
-
storedMessages: import("../constants").MessageWithPartsLike[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface TriggerMatch {
|
|
17
|
-
partArrayIndex: number
|
|
18
|
-
eligibleIndex: number
|
|
19
|
-
keyword: string
|
|
20
|
-
offset: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface TranslatorLike {
|
|
24
|
-
translateText(input: {
|
|
25
|
-
text: string
|
|
26
|
-
sourceLanguage: string
|
|
27
|
-
targetLanguage: string
|
|
28
|
-
direction: "inbound" | "outbound"
|
|
29
|
-
}): Promise<string>
|
|
30
|
-
translateTexts?(input: {
|
|
31
|
-
texts: readonly string[]
|
|
32
|
-
sourceLanguage: string
|
|
33
|
-
targetLanguage: string
|
|
34
|
-
direction: "inbound" | "outbound"
|
|
35
|
-
}): Promise<readonly string[]>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface HookDependencies {
|
|
39
|
-
translator?: TranslatorLike
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface HookContext {
|
|
43
|
-
client: PluginClientLike
|
|
44
|
-
directory?: string
|
|
45
|
-
options: ResolvedTranslateOptions
|
|
46
|
-
translator: TranslatorLike
|
|
47
|
-
}
|
package/src/activation.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./activation/index"
|
package/src/anthropic-oauth.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// Anthropic OAuth request transformations.
|
|
2
|
-
//
|
|
3
|
-
// When using Anthropic's OAuth credentials (Claude Pro/Max) outside of the
|
|
4
|
-
// official Claude Code client, the /v1/messages API responds with a
|
|
5
|
-
// `429 rate_limit_error` and an empty "Error" message unless the request
|
|
6
|
-
// shape matches the Claude Code CLI fingerprint: specific headers, a
|
|
7
|
-
// `?beta=true` query, the Claude Code identity in `system[0]`, and a
|
|
8
|
-
// deterministic billing header block.
|
|
9
|
-
//
|
|
10
|
-
// This module implements the minimum transformation that makes translator
|
|
11
|
-
// requests pass those checks. The technique (including the identity string,
|
|
12
|
-
// required beta headers, and CCH billing header format) is documented by the
|
|
13
|
-
// `@ex-machina/opencode-anthropic-auth` plugin:
|
|
14
|
-
//
|
|
15
|
-
// https://github.com/ex-machina-co/opencode-anthropic-auth
|
|
16
|
-
//
|
|
17
|
-
// We only apply these transformations to translator requests this plugin
|
|
18
|
-
// originates. OpenCode's main chat already has its own auth loader (e.g.
|
|
19
|
-
// `@ex-machina/opencode-anthropic-auth`) handling its requests independently.
|
|
20
|
-
|
|
21
|
-
import { createHash } from "node:crypto"
|
|
22
|
-
|
|
23
|
-
export const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK."
|
|
24
|
-
|
|
25
|
-
const REQUIRED_BETAS = ["oauth-2025-04-20", "interleaved-thinking-2025-05-14"] as const
|
|
26
|
-
|
|
27
|
-
const CLAUDE_CODE_VERSION = "2.1.87"
|
|
28
|
-
const CLAUDE_CODE_ENTRYPOINT = "sdk-cli"
|
|
29
|
-
export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`
|
|
30
|
-
|
|
31
|
-
// Deterministic billing header parameters — must match Claude Code's own derivation.
|
|
32
|
-
const CCH_SALT = "59cf53e54c78"
|
|
33
|
-
const CCH_POSITIONS = [4, 7, 20] as const
|
|
34
|
-
|
|
35
|
-
type SystemBlock = { type: string; text: string; [key: string]: unknown }
|
|
36
|
-
|
|
37
|
-
type MessageLike = {
|
|
38
|
-
role?: string
|
|
39
|
-
content?: string | Array<{ type?: string; text?: string }>
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
43
|
-
return value != null && typeof value === "object" && !Array.isArray(value)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function extractFirstUserMessageText(messages: MessageLike[] | undefined): string {
|
|
47
|
-
if (!Array.isArray(messages)) return ""
|
|
48
|
-
const first = messages.find((message) => message?.role === "user")
|
|
49
|
-
if (!first) return ""
|
|
50
|
-
const { content } = first
|
|
51
|
-
if (typeof content === "string") return content
|
|
52
|
-
if (Array.isArray(content)) {
|
|
53
|
-
const textBlock = content.find((block) => block?.type === "text")
|
|
54
|
-
if (textBlock?.text) return textBlock.text
|
|
55
|
-
}
|
|
56
|
-
return ""
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function computeCCH(messageText: string): string {
|
|
60
|
-
return createHash("sha256").update(messageText).digest("hex").slice(0, 5)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function computeVersionSuffix(messageText: string, version: string): string {
|
|
64
|
-
const chars = CCH_POSITIONS.map((index) => messageText[index] ?? "0").join("")
|
|
65
|
-
return createHash("sha256").update(`${CCH_SALT}${chars}${version}`).digest("hex").slice(0, 3)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function buildBillingHeaderValue(messages: MessageLike[] | undefined): string {
|
|
69
|
-
const text = extractFirstUserMessageText(messages)
|
|
70
|
-
const cch = computeCCH(text)
|
|
71
|
-
const suffix = computeVersionSuffix(text, CLAUDE_CODE_VERSION)
|
|
72
|
-
return (
|
|
73
|
-
"x-anthropic-billing-header: " +
|
|
74
|
-
`cc_version=${CLAUDE_CODE_VERSION}.${suffix}; ` +
|
|
75
|
-
`cc_entrypoint=${CLAUDE_CODE_ENTRYPOINT}; ` +
|
|
76
|
-
`cch=${cch};`
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function mergeBetaHeaders(headers: Headers): string {
|
|
81
|
-
const incoming = headers.get("anthropic-beta") || ""
|
|
82
|
-
const incomingList = incoming
|
|
83
|
-
.split(",")
|
|
84
|
-
.map((value) => value.trim())
|
|
85
|
-
.filter(Boolean)
|
|
86
|
-
return [...new Set([...REQUIRED_BETAS, ...incomingList])].join(",")
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function setOAuthHeaders(headers: Headers, accessToken: string): Headers {
|
|
90
|
-
headers.set("authorization", `Bearer ${accessToken}`)
|
|
91
|
-
headers.set("anthropic-beta", mergeBetaHeaders(headers))
|
|
92
|
-
headers.set("user-agent", CLAUDE_CLI_USER_AGENT)
|
|
93
|
-
headers.delete("x-api-key")
|
|
94
|
-
return headers
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function rewriteMessagesURL(input: URL): URL {
|
|
98
|
-
if (input.pathname === "/v1/messages" && !input.searchParams.has("beta")) {
|
|
99
|
-
input.searchParams.set("beta", "true")
|
|
100
|
-
}
|
|
101
|
-
return input
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function normalizeSystem(raw: unknown): SystemBlock[] {
|
|
105
|
-
if (raw == null) return []
|
|
106
|
-
if (typeof raw === "string") return raw.length > 0 ? [{ type: "text", text: raw }] : []
|
|
107
|
-
if (isRecord(raw)) {
|
|
108
|
-
const type = typeof raw.type === "string" ? raw.type : "text"
|
|
109
|
-
const text = typeof raw.text === "string" ? raw.text : ""
|
|
110
|
-
return [{ ...raw, type, text }]
|
|
111
|
-
}
|
|
112
|
-
if (!Array.isArray(raw)) return []
|
|
113
|
-
return raw
|
|
114
|
-
.map((item): SystemBlock | null => {
|
|
115
|
-
if (typeof item === "string") return { type: "text", text: item }
|
|
116
|
-
if (isRecord(item) && typeof item.text === "string") {
|
|
117
|
-
const type = typeof item.type === "string" ? item.type : "text"
|
|
118
|
-
return { ...item, type, text: item.text }
|
|
119
|
-
}
|
|
120
|
-
return null
|
|
121
|
-
})
|
|
122
|
-
.filter((block): block is SystemBlock => block !== null)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function buildOAuthSystem(rawSystem: unknown, messages: MessageLike[] | undefined): SystemBlock[] {
|
|
126
|
-
const identity: SystemBlock = { type: "text", text: CLAUDE_CODE_IDENTITY }
|
|
127
|
-
const existing = normalizeSystem(rawSystem).filter((block) => block.text !== CLAUDE_CODE_IDENTITY)
|
|
128
|
-
const billing: SystemBlock = { type: "text", text: buildBillingHeaderValue(messages) }
|
|
129
|
-
return [billing, identity, ...existing]
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Rewrite an /v1/messages POST body so its system prompt and billing header
|
|
133
|
-
// satisfy Claude Code's OAuth fingerprint. Returns the original body on parse
|
|
134
|
-
// failure so we never break a request that was already well-formed.
|
|
135
|
-
export function rewriteMessagesBody(body: string): string {
|
|
136
|
-
try {
|
|
137
|
-
const parsed = JSON.parse(body) as Record<string, unknown>
|
|
138
|
-
const messages = Array.isArray(parsed.messages) ? (parsed.messages as MessageLike[]) : undefined
|
|
139
|
-
parsed.system = buildOAuthSystem(parsed.system, messages)
|
|
140
|
-
return JSON.stringify(parsed)
|
|
141
|
-
} catch {
|
|
142
|
-
return body
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function isAnthropicMessagesRequest(url: URL): boolean {
|
|
147
|
-
return url.pathname === "/v1/messages"
|
|
148
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { isRecord } from "./codex-shared"
|
|
2
|
-
import type { CodexBodyRewrite } from "./types"
|
|
3
|
-
|
|
4
|
-
function textFromContent(content: unknown): string | undefined {
|
|
5
|
-
if (typeof content === "string") return content
|
|
6
|
-
if (!Array.isArray(content)) return undefined
|
|
7
|
-
|
|
8
|
-
const text = content
|
|
9
|
-
.map((part) => (isRecord(part) && typeof part.text === "string" ? part.text : undefined))
|
|
10
|
-
.filter((value): value is string => value !== undefined)
|
|
11
|
-
.join("\n")
|
|
12
|
-
|
|
13
|
-
return text || undefined
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function normalizeCodexContent(role: string, content: unknown): Record<string, unknown>[] {
|
|
17
|
-
const textType = role === "assistant" ? "output_text" : "input_text"
|
|
18
|
-
if (typeof content === "string") return [{ type: textType, text: content }]
|
|
19
|
-
if (!Array.isArray(content)) return []
|
|
20
|
-
|
|
21
|
-
const result: Record<string, unknown>[] = []
|
|
22
|
-
for (const part of content) {
|
|
23
|
-
if (!isRecord(part)) continue
|
|
24
|
-
const type = part.type
|
|
25
|
-
if (type === "input_text" || type === "output_text") {
|
|
26
|
-
result.push({ ...part, type: textType })
|
|
27
|
-
continue
|
|
28
|
-
}
|
|
29
|
-
if (type === "input_image") {
|
|
30
|
-
result.push({ ...part })
|
|
31
|
-
continue
|
|
32
|
-
}
|
|
33
|
-
if (typeof part.text === "string") result.push({ type: textType, text: part.text })
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return result
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function normalizeCodexInputItem(item: unknown, instructions: string[]): unknown | undefined {
|
|
40
|
-
if (!isRecord(item)) return item
|
|
41
|
-
const role = typeof item.role === "string" ? item.role : undefined
|
|
42
|
-
|
|
43
|
-
if (role === "system" || role === "developer") {
|
|
44
|
-
const text = textFromContent(item.content)
|
|
45
|
-
if (text) instructions.push(text)
|
|
46
|
-
return undefined
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (item.type === "message" && role) {
|
|
50
|
-
const content = normalizeCodexContent(role, item.content)
|
|
51
|
-
return content.length > 0 ? { ...item, role, content } : undefined
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (role) {
|
|
55
|
-
const content = normalizeCodexContent(role, item.content)
|
|
56
|
-
return content.length > 0 ? { type: "message", role, content } : undefined
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return item
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function rewriteOpenAICodexBody(body: BodyInit | null | undefined): CodexBodyRewrite {
|
|
63
|
-
if (typeof body !== "string") return { body, originalStream: false }
|
|
64
|
-
|
|
65
|
-
let parsed: unknown
|
|
66
|
-
try {
|
|
67
|
-
parsed = JSON.parse(body)
|
|
68
|
-
} catch {
|
|
69
|
-
return { body, originalStream: false }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!isRecord(parsed)) return { body, originalStream: false }
|
|
73
|
-
const originalStream = parsed.stream === true
|
|
74
|
-
const sourceInput = Array.isArray(parsed.input)
|
|
75
|
-
? parsed.input
|
|
76
|
-
: Array.isArray(parsed.messages)
|
|
77
|
-
? parsed.messages
|
|
78
|
-
: undefined
|
|
79
|
-
if (!sourceInput) return { body, originalStream }
|
|
80
|
-
|
|
81
|
-
const instructions: string[] = []
|
|
82
|
-
if (typeof parsed.instructions === "string" && parsed.instructions) instructions.push(parsed.instructions)
|
|
83
|
-
const input = sourceInput
|
|
84
|
-
.map((item) => normalizeCodexInputItem(item, instructions))
|
|
85
|
-
.filter((item): item is unknown => item !== undefined)
|
|
86
|
-
const include = Array.isArray(parsed.include)
|
|
87
|
-
? parsed.include.filter((item): item is string => typeof item === "string")
|
|
88
|
-
: []
|
|
89
|
-
if (!include.includes("reasoning.encrypted_content")) include.push("reasoning.encrypted_content")
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
body: JSON.stringify({
|
|
93
|
-
...parsed,
|
|
94
|
-
instructions: instructions.join("\n\n"),
|
|
95
|
-
input,
|
|
96
|
-
tools: Array.isArray(parsed.tools) ? parsed.tools : [],
|
|
97
|
-
tool_choice: typeof parsed.tool_choice === "string" ? parsed.tool_choice : "auto",
|
|
98
|
-
parallel_tool_calls: typeof parsed.parallel_tool_calls === "boolean" ? parsed.parallel_tool_calls : false,
|
|
99
|
-
store: false,
|
|
100
|
-
stream: true,
|
|
101
|
-
include,
|
|
102
|
-
max_output_tokens: undefined,
|
|
103
|
-
max_completion_tokens: undefined,
|
|
104
|
-
messages: undefined,
|
|
105
|
-
}),
|
|
106
|
-
originalStream,
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { isRecord } from "./codex-shared"
|
|
2
|
-
|
|
3
|
-
function normalizeCodexOutputItem(item: unknown, index: number): unknown | undefined {
|
|
4
|
-
if (!isRecord(item)) return undefined
|
|
5
|
-
if (item.type !== "message" || item.role !== "assistant") return item
|
|
6
|
-
if (!Array.isArray(item.content)) return undefined
|
|
7
|
-
|
|
8
|
-
const content: Record<string, unknown>[] = []
|
|
9
|
-
for (const part of item.content) {
|
|
10
|
-
if (!isRecord(part) || part.type !== "output_text" || typeof part.text !== "string") continue
|
|
11
|
-
content.push({ ...part, annotations: Array.isArray(part.annotations) ? part.annotations : [] })
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (content.length === 0) return undefined
|
|
15
|
-
return {
|
|
16
|
-
...item,
|
|
17
|
-
id: typeof item.id === "string" ? item.id : `msg_opencode_translate_${index}`,
|
|
18
|
-
role: "assistant",
|
|
19
|
-
content,
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function buildCodexTextOutput(text: string): Record<string, unknown> {
|
|
24
|
-
return {
|
|
25
|
-
type: "message",
|
|
26
|
-
id: "msg_opencode_translate_0",
|
|
27
|
-
role: "assistant",
|
|
28
|
-
content: [{ type: "output_text", text, annotations: [] }],
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseCodexSSEResponse(text: string): unknown | undefined {
|
|
33
|
-
let finalResponse: unknown
|
|
34
|
-
let deltaText = ""
|
|
35
|
-
const outputItems: unknown[] = []
|
|
36
|
-
|
|
37
|
-
for (const line of text.split(/\r?\n/)) {
|
|
38
|
-
if (!line.startsWith("data: ")) continue
|
|
39
|
-
const payload = line.slice(6).trim()
|
|
40
|
-
if (!payload || payload === "[DONE]") continue
|
|
41
|
-
try {
|
|
42
|
-
const parsed = JSON.parse(payload) as Record<string, unknown>
|
|
43
|
-
if (parsed.type === "response.output_text.delta" && typeof parsed.delta === "string") {
|
|
44
|
-
deltaText += parsed.delta
|
|
45
|
-
} else if (
|
|
46
|
-
(parsed.type === "response.output_item.done" || parsed.type === "response.output_item.added") &&
|
|
47
|
-
parsed.item
|
|
48
|
-
) {
|
|
49
|
-
outputItems.push(parsed.item)
|
|
50
|
-
} else if ((parsed.type === "response.done" || parsed.type === "response.completed") && parsed.response) {
|
|
51
|
-
finalResponse = parsed.response
|
|
52
|
-
}
|
|
53
|
-
} catch {}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!finalResponse && !deltaText && outputItems.length === 0) return undefined
|
|
57
|
-
const response: Record<string, unknown> = isRecord(finalResponse)
|
|
58
|
-
? { ...finalResponse }
|
|
59
|
-
: { id: "resp_opencode_translate" }
|
|
60
|
-
const existingOutput: unknown[] = Array.isArray(response.output) ? response.output : []
|
|
61
|
-
const sourceOutput = existingOutput.length > 0 ? existingOutput : outputItems
|
|
62
|
-
const normalizedOutput = sourceOutput
|
|
63
|
-
.map((item, index) => normalizeCodexOutputItem(item, index))
|
|
64
|
-
.filter((item): item is unknown => item !== undefined)
|
|
65
|
-
|
|
66
|
-
response.output = normalizedOutput.length > 0 ? normalizedOutput : deltaText ? [buildCodexTextOutput(deltaText)] : []
|
|
67
|
-
return response
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function convertCodexSSEToJSON(response: Response): Promise<Response> {
|
|
71
|
-
const headers = new Headers(response.headers)
|
|
72
|
-
const text = await response.text()
|
|
73
|
-
const parsed = parseCodexSSEResponse(text)
|
|
74
|
-
if (!parsed) return new Response(text, { status: response.status, statusText: response.statusText, headers })
|
|
75
|
-
|
|
76
|
-
headers.set("content-type", "application/json; charset=utf-8")
|
|
77
|
-
return new Response(JSON.stringify(parsed), { status: response.status, statusText: response.statusText, headers })
|
|
78
|
-
}
|
package/src/auth/codex-shared.ts
DELETED
package/src/auth/headers.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { USER_AGENT } from "../constants"
|
|
2
|
-
|
|
3
|
-
export function copyHeaders(headers?: HeadersInit): Headers {
|
|
4
|
-
return new Headers(headers)
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function headerValue(headers: Headers, key: string): string | undefined {
|
|
8
|
-
const value = headers.get(key)
|
|
9
|
-
return value === null ? undefined : value
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function packageUserAgent(packageVersion?: string): string {
|
|
13
|
-
return packageVersion ? USER_AGENT.replace("0.0.0", packageVersion) : USER_AGENT
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function setUserAgent(headers: Headers, packageVersion?: string) {
|
|
17
|
-
headers.set("User-Agent", packageUserAgent(packageVersion))
|
|
18
|
-
}
|