mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-10 02:44:21 +00:00
Compare commits
7 Commits
v1.1.52
...
codex-auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0fd15e9e4 | ||
|
|
4f2514d9f5 | ||
|
|
5d9801f206 | ||
|
|
a300cd6465 | ||
|
|
399699469b | ||
|
|
cacf9df24c | ||
|
|
2844086752 |
@@ -3,6 +3,8 @@ import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
import z from "zod"
|
||||
|
||||
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
|
||||
|
||||
export namespace Auth {
|
||||
export const Oauth = z
|
||||
.object({
|
||||
|
||||
417
packages/opencode/src/plugin/codex.ts
Normal file
417
packages/opencode/src/plugin/codex.ts
Normal file
@@ -0,0 +1,417 @@
|
||||
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
||||
import { Log } from "../util/log"
|
||||
import { OAUTH_DUMMY_KEY } from "../auth"
|
||||
|
||||
const log = Log.create({ service: "plugin.codex" })
|
||||
|
||||
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||
const ISSUER = "https://auth.openai.com"
|
||||
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
|
||||
const OAUTH_PORT = 1455
|
||||
|
||||
interface PkceCodes {
|
||||
verifier: string
|
||||
challenge: string
|
||||
}
|
||||
|
||||
async function generatePKCE(): Promise<PkceCodes> {
|
||||
const verifier = generateRandomString(43)
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(verifier)
|
||||
const hash = await crypto.subtle.digest("SHA-256", data)
|
||||
const challenge = base64UrlEncode(hash)
|
||||
return { verifier, challenge }
|
||||
}
|
||||
|
||||
function generateRandomString(length: number): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(length))
|
||||
return Array.from(bytes)
|
||||
.map((b) => chars[b % chars.length])
|
||||
.join("")
|
||||
}
|
||||
|
||||
function base64UrlEncode(buffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(buffer)
|
||||
const binary = String.fromCharCode(...bytes)
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
||||
}
|
||||
|
||||
function generateState(): string {
|
||||
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer)
|
||||
}
|
||||
|
||||
function buildAuthorizeUrl(redirectUri: string, pkce: PkceCodes, state: string): string {
|
||||
const params = new URLSearchParams({
|
||||
response_type: "code",
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: redirectUri,
|
||||
scope: "openid profile email offline_access",
|
||||
code_challenge: pkce.challenge,
|
||||
code_challenge_method: "S256",
|
||||
id_token_add_organizations: "true",
|
||||
codex_cli_simplified_flow: "true",
|
||||
state,
|
||||
originator: "opencode",
|
||||
})
|
||||
return `${ISSUER}/oauth/authorize?${params.toString()}`
|
||||
}
|
||||
|
||||
interface TokenResponse {
|
||||
id_token: string
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in?: number
|
||||
}
|
||||
|
||||
async function exchangeCodeForTokens(code: string, redirectUri: string, pkce: PkceCodes): Promise<TokenResponse> {
|
||||
const response = await fetch(`${ISSUER}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
client_id: CLIENT_ID,
|
||||
code_verifier: pkce.verifier,
|
||||
}).toString(),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Token exchange failed: ${response.status}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
async function refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
|
||||
const response = await fetch(`${ISSUER}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: CLIENT_ID,
|
||||
}).toString(),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Token refresh failed: ${response.status}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
const HTML_SUCCESS = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenCode - Codex Authorization Successful</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
|
||||
.container { text-align: center; padding: 2rem; }
|
||||
h1 { color: #4ade80; margin-bottom: 1rem; }
|
||||
p { color: #aaa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Authorization Successful</h1>
|
||||
<p>You can close this window and return to OpenCode.</p>
|
||||
</div>
|
||||
<script>setTimeout(() => window.close(), 2000);</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
const HTML_ERROR = (error: string) => `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenCode - Codex Authorization Failed</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
|
||||
.container { text-align: center; padding: 2rem; }
|
||||
h1 { color: #f87171; margin-bottom: 1rem; }
|
||||
p { color: #aaa; }
|
||||
.error { color: #fca5a5; font-family: monospace; margin-top: 1rem; padding: 1rem; background: rgba(248,113,113,0.1); border-radius: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Authorization Failed</h1>
|
||||
<p>An error occurred during authorization.</p>
|
||||
<div class="error">${error}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
interface PendingOAuth {
|
||||
pkce: PkceCodes
|
||||
state: string
|
||||
resolve: (tokens: TokenResponse) => void
|
||||
reject: (error: Error) => void
|
||||
}
|
||||
|
||||
let oauthServer: ReturnType<typeof Bun.serve> | undefined
|
||||
let pendingOAuth: PendingOAuth | undefined
|
||||
|
||||
async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> {
|
||||
if (oauthServer) {
|
||||
return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
|
||||
}
|
||||
|
||||
oauthServer = Bun.serve({
|
||||
port: OAUTH_PORT,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
|
||||
if (url.pathname === "/auth/callback") {
|
||||
const code = url.searchParams.get("code")
|
||||
const state = url.searchParams.get("state")
|
||||
const error = url.searchParams.get("error")
|
||||
const errorDescription = url.searchParams.get("error_description")
|
||||
|
||||
if (error) {
|
||||
const errorMsg = errorDescription || error
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
return new Response(HTML_ERROR(errorMsg), {
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
const errorMsg = "Missing authorization code"
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
return new Response(HTML_ERROR(errorMsg), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
if (!pendingOAuth || state !== pendingOAuth.state) {
|
||||
const errorMsg = "Invalid state - potential CSRF attack"
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
return new Response(HTML_ERROR(errorMsg), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
const current = pendingOAuth
|
||||
pendingOAuth = undefined
|
||||
|
||||
exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce)
|
||||
.then((tokens) => current.resolve(tokens))
|
||||
.catch((err) => current.reject(err))
|
||||
|
||||
return new Response(HTML_SUCCESS, {
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
if (url.pathname === "/cancel") {
|
||||
pendingOAuth?.reject(new Error("Login cancelled"))
|
||||
pendingOAuth = undefined
|
||||
return new Response("Login cancelled", { status: 200 })
|
||||
}
|
||||
|
||||
return new Response("Not found", { status: 404 })
|
||||
},
|
||||
})
|
||||
|
||||
log.info("codex oauth server started", { port: OAUTH_PORT })
|
||||
return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
|
||||
}
|
||||
|
||||
function stopOAuthServer() {
|
||||
if (oauthServer) {
|
||||
oauthServer.stop()
|
||||
oauthServer = undefined
|
||||
log.info("codex oauth server stopped")
|
||||
}
|
||||
}
|
||||
|
||||
function waitForOAuthCallback(pkce: PkceCodes, state: string): Promise<TokenResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
if (pendingOAuth) {
|
||||
pendingOAuth = undefined
|
||||
reject(new Error("OAuth callback timeout - authorization took too long"))
|
||||
}
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
) // 5 minute timeout
|
||||
|
||||
pendingOAuth = {
|
||||
pkce,
|
||||
state,
|
||||
resolve: (tokens) => {
|
||||
clearTimeout(timeout)
|
||||
resolve(tokens)
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
return {
|
||||
auth: {
|
||||
provider: "openai",
|
||||
async loader(getAuth, provider) {
|
||||
const auth = await getAuth()
|
||||
if (auth.type !== "oauth") return {}
|
||||
|
||||
// Filter models to only allowed Codex models for OAuth
|
||||
const allowedModels = new Set(["gpt-5.1-codex-max", "gpt-5.1-codex-mini", "gpt-5.2", "gpt-5.2-codex"])
|
||||
for (const modelId of Object.keys(provider.models)) {
|
||||
if (!allowedModels.has(modelId)) {
|
||||
delete provider.models[modelId]
|
||||
}
|
||||
}
|
||||
|
||||
if (!provider.models["gpt-5.2-codex"]) {
|
||||
provider.models["gpt-5.2-codex"] = {
|
||||
id: "gpt-5.2-codex",
|
||||
providerID: "openai",
|
||||
api: {
|
||||
id: "gpt-5.2-codex",
|
||||
url: "https://chatgpt.com/backend-api/codex",
|
||||
npm: "@ai-sdk/openai",
|
||||
},
|
||||
name: "GPT-5.2 Codex",
|
||||
capabilities: {
|
||||
temperature: false,
|
||||
reasoning: true,
|
||||
attachment: true,
|
||||
toolcall: true,
|
||||
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
||||
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||
},
|
||||
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||
limit: { context: 400000, output: 128000 },
|
||||
status: "active",
|
||||
options: {},
|
||||
headers: {},
|
||||
}
|
||||
}
|
||||
|
||||
// Zero out costs for Codex (included with ChatGPT subscription)
|
||||
for (const model of Object.values(provider.models)) {
|
||||
model.cost = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
apiKey: OAUTH_DUMMY_KEY,
|
||||
async fetch(requestInput: RequestInfo | URL, init?: RequestInit) {
|
||||
// Remove dummy API key authorization header
|
||||
if (init?.headers) {
|
||||
if (init.headers instanceof Headers) {
|
||||
init.headers.delete("authorization")
|
||||
init.headers.delete("Authorization")
|
||||
} else if (Array.isArray(init.headers)) {
|
||||
init.headers = init.headers.filter(([key]) => key.toLowerCase() !== "authorization")
|
||||
} else {
|
||||
delete init.headers["authorization"]
|
||||
delete init.headers["Authorization"]
|
||||
}
|
||||
}
|
||||
|
||||
const currentAuth = await getAuth()
|
||||
if (currentAuth.type !== "oauth") return fetch(requestInput, init)
|
||||
|
||||
// Check if token needs refresh
|
||||
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
||||
log.info("refreshing codex access token")
|
||||
const tokens = await refreshAccessToken(currentAuth.refresh)
|
||||
await input.client.auth.set({
|
||||
path: { id: "codex" },
|
||||
body: {
|
||||
type: "oauth",
|
||||
refresh: tokens.refresh_token,
|
||||
access: tokens.access_token,
|
||||
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
||||
},
|
||||
})
|
||||
currentAuth.access = tokens.access_token
|
||||
}
|
||||
|
||||
// Build headers
|
||||
const headers = new Headers()
|
||||
if (init?.headers) {
|
||||
if (init.headers instanceof Headers) {
|
||||
init.headers.forEach((value, key) => headers.set(key, value))
|
||||
} else if (Array.isArray(init.headers)) {
|
||||
for (const [key, value] of init.headers) {
|
||||
if (value !== undefined) headers.set(key, String(value))
|
||||
}
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(init.headers)) {
|
||||
if (value !== undefined) headers.set(key, String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set authorization header with access token
|
||||
headers.set("authorization", `Bearer ${currentAuth.access}`)
|
||||
|
||||
// Rewrite URL to Codex endpoint
|
||||
let url: URL
|
||||
if (typeof requestInput === "string") {
|
||||
url = new URL(requestInput)
|
||||
} else if (requestInput instanceof URL) {
|
||||
url = requestInput
|
||||
} else {
|
||||
url = new URL(requestInput.url)
|
||||
}
|
||||
|
||||
// If this is a messages/responses request, redirect to Codex endpoint
|
||||
if (url.pathname.includes("/v1/responses") || url.pathname.includes("/chat/completions")) {
|
||||
url = new URL(CODEX_API_ENDPOINT)
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
...init,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: [
|
||||
{
|
||||
label: "ChatGPT Pro/Plus",
|
||||
type: "oauth",
|
||||
authorize: async () => {
|
||||
const { redirectUri } = await startOAuthServer()
|
||||
const pkce = await generatePKCE()
|
||||
const state = generateState()
|
||||
const authUrl = buildAuthorizeUrl(redirectUri, pkce, state)
|
||||
|
||||
const callbackPromise = waitForOAuthCallback(pkce, state)
|
||||
|
||||
return {
|
||||
url: authUrl,
|
||||
instructions: "Complete authorization in your browser. This window will close automatically.",
|
||||
method: "auto" as const,
|
||||
callback: async () => {
|
||||
const tokens = await callbackPromise
|
||||
stopOAuthServer()
|
||||
return {
|
||||
type: "success" as const,
|
||||
refresh: tokens.refresh_token,
|
||||
access: tokens.access_token,
|
||||
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,16 @@ import { Server } from "../server/server"
|
||||
import { BunProc } from "../bun"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { CodexAuthPlugin } from "./codex"
|
||||
|
||||
export namespace Plugin {
|
||||
const log = Log.create({ service: "plugin" })
|
||||
|
||||
const BUILTIN = ["opencode-copilot-auth@0.0.11", "opencode-anthropic-auth@0.0.8"]
|
||||
|
||||
// Built-in plugins that are directly imported (not installed from npm)
|
||||
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin]
|
||||
|
||||
const state = Instance.state(async () => {
|
||||
const client = createOpencodeClient({
|
||||
baseUrl: "http://localhost:4096",
|
||||
@@ -20,7 +24,7 @@ export namespace Plugin {
|
||||
fetch: async (...args) => Server.App().fetch(...args),
|
||||
})
|
||||
const config = await Config.get()
|
||||
const hooks = []
|
||||
const hooks: Hooks[] = []
|
||||
const input: PluginInput = {
|
||||
client,
|
||||
project: Instance.project,
|
||||
@@ -29,11 +33,23 @@ export namespace Plugin {
|
||||
serverUrl: Server.url(),
|
||||
$: Bun.$,
|
||||
}
|
||||
|
||||
// Load internal plugins first
|
||||
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
||||
for (const plugin of INTERNAL_PLUGINS) {
|
||||
log.info("loading internal plugin", { name: plugin.name })
|
||||
const init = await plugin(input)
|
||||
hooks.push(init)
|
||||
}
|
||||
}
|
||||
|
||||
const plugins = [...(config.plugin ?? [])]
|
||||
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
||||
plugins.push(...BUILTIN)
|
||||
}
|
||||
for (let plugin of plugins) {
|
||||
// ignore old codex plugin since it is supported first party now
|
||||
if (plugin.includes("opencode-openai-codex-auth")) continue
|
||||
log.info("loading plugin", { path: plugin })
|
||||
if (!plugin.startsWith("file://")) {
|
||||
const lastAtIndex = plugin.lastIndexOf("@")
|
||||
|
||||
@@ -74,6 +74,7 @@ export namespace Server {
|
||||
const app = new Hono()
|
||||
export const App: () => Hono = lazy(
|
||||
() =>
|
||||
// TODO: Break server.ts into smaller route files to fix type inference
|
||||
app
|
||||
.onError((err, c) => {
|
||||
log.error("failed", {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os from "os"
|
||||
import { Installation } from "@/installation"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Log } from "@/util/log"
|
||||
import {
|
||||
@@ -19,6 +21,7 @@ import { Plugin } from "@/plugin"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
import { Auth } from "@/auth"
|
||||
|
||||
export namespace LLM {
|
||||
const log = Log.create({ service: "llm" })
|
||||
@@ -82,12 +85,24 @@ export namespace LLM {
|
||||
}
|
||||
|
||||
const provider = await Provider.getProvider(input.model.providerID)
|
||||
const auth = await Auth.get(input.model.providerID)
|
||||
const isCodex = provider.id === "openai" && auth?.type === "oauth"
|
||||
|
||||
const variant =
|
||||
!input.small && input.model.variants && input.user.variant ? input.model.variants[input.user.variant] : {}
|
||||
const base = input.small
|
||||
? ProviderTransform.smallOptions(input.model)
|
||||
: ProviderTransform.options(input.model, input.sessionID, provider.options)
|
||||
const options = pipe(base, mergeDeep(input.model.options), mergeDeep(input.agent.options), mergeDeep(variant))
|
||||
const options: Record<string, any> = pipe(
|
||||
base,
|
||||
mergeDeep(input.model.options),
|
||||
mergeDeep(input.agent.options),
|
||||
mergeDeep(variant),
|
||||
)
|
||||
if (isCodex) {
|
||||
options.instructions = SystemPrompt.instructions()
|
||||
options.store = false
|
||||
}
|
||||
|
||||
const params = await Plugin.trigger(
|
||||
"chat.params",
|
||||
@@ -108,16 +123,14 @@ export namespace LLM {
|
||||
},
|
||||
)
|
||||
|
||||
l.info("params", {
|
||||
params,
|
||||
})
|
||||
|
||||
const maxOutputTokens = ProviderTransform.maxOutputTokens(
|
||||
input.model.api.npm,
|
||||
params.options,
|
||||
input.model.limit.output,
|
||||
OUTPUT_TOKEN_MAX,
|
||||
)
|
||||
const maxOutputTokens = isCodex
|
||||
? undefined
|
||||
: ProviderTransform.maxOutputTokens(
|
||||
input.model.api.npm,
|
||||
params.options,
|
||||
input.model.limit.output,
|
||||
OUTPUT_TOKEN_MAX,
|
||||
)
|
||||
|
||||
const tools = await resolveTools(input)
|
||||
|
||||
@@ -157,6 +170,13 @@ export namespace LLM {
|
||||
maxOutputTokens,
|
||||
abortSignal: input.abort,
|
||||
headers: {
|
||||
...(isCodex
|
||||
? {
|
||||
originator: "opencode",
|
||||
"User-Agent": `opencode/${Installation.VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
session_id: input.sessionID,
|
||||
}
|
||||
: undefined),
|
||||
...(input.model.providerID.startsWith("opencode")
|
||||
? {
|
||||
"x-opencode-project": Instance.project.id,
|
||||
@@ -169,12 +189,19 @@ export namespace LLM {
|
||||
},
|
||||
maxRetries: input.retries ?? 0,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
...(isCodex
|
||||
? [
|
||||
{
|
||||
role: "user",
|
||||
content: system.join("\n\n"),
|
||||
} as ModelMessage,
|
||||
]
|
||||
: system.map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
)),
|
||||
...input.messages,
|
||||
],
|
||||
model: wrapLanguageModel({
|
||||
|
||||
1
packages/opencode/src/session/prompt/codex_header.txt
Normal file
1
packages/opencode/src/session/prompt/codex_header.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are a coding agent running in the opencode, a terminal-based coding assistant. opencode is an open source project. You are expected to be precise, safe, and helpful.
|
||||
@@ -14,6 +14,7 @@ import PROMPT_GEMINI from "./prompt/gemini.txt"
|
||||
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
||||
|
||||
import PROMPT_CODEX from "./prompt/codex.txt"
|
||||
import PROMPT_CODEX_INSTRUCTIONS from "./prompt/codex_header.txt"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { Flag } from "@/flag/flag"
|
||||
|
||||
@@ -23,6 +24,10 @@ export namespace SystemPrompt {
|
||||
return []
|
||||
}
|
||||
|
||||
export function instructions() {
|
||||
return PROMPT_CODEX_INSTRUCTIONS.trim()
|
||||
}
|
||||
|
||||
export function provider(model: Provider.Model) {
|
||||
if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX]
|
||||
if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3"))
|
||||
|
||||
Reference in New Issue
Block a user