diff --git a/.gitignore b/.gitignore index 00128ff05..e17a38369 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ dist/ cli cli-dev +openclaw/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..43a426911 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,47 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common commands + +```bash +# Install dependencies +bun install + +# Standard build (./cli) +bun run build + +# Dev build (./cli-dev) +bun run build:dev + +# Dev build with all experimental features (./cli-dev) +bun run build:dev:full + +# Compiled build (./dist/cli) +bun run compile + +# Run from source without compiling +bun run dev +``` + +Run the built binary with `./cli` or `./cli-dev`. Set `ANTHROPIC_API_KEY` in the environment or use OAuth via `./cli /login`. + +## High-level architecture + +- **Entry point/UI loop**: src/entrypoints/cli.tsx bootstraps the CLI, with the main interactive UI in src/screens/REPL.tsx (Ink/React). +- **Command/tool registries**: src/commands.ts registers slash commands; src/tools.ts registers tool implementations. Implementations live in src/commands/ and src/tools/. +- **LLM query pipeline**: src/QueryEngine.ts coordinates message flow, tool use, and model invocation. +- **Core subsystems**: + - src/services/: API clients, OAuth/MCP integration, analytics stubs + - src/state/: app state store + - src/hooks/: React hooks used by UI/flows + - src/components/: terminal UI components (Ink) + - src/skills/: skill system + - src/plugins/: plugin system + - src/bridge/: IDE bridge + - src/voice/: voice input + - src/tasks/: background task management + +## Build system + +- scripts/build.ts is the build script and feature-flag bundler. Feature flags are set via build arguments (e.g., `--feature=ULTRAPLAN`) or presets like `--feature-set=dev-full` (see README for details). \ No newline at end of file diff --git a/changes.md b/changes.md new file mode 100644 index 000000000..c025fdae5 --- /dev/null +++ b/changes.md @@ -0,0 +1,24 @@ +# Codex API Support: Feature Parity & UI Overhaul + +## Summary +This pull request introduces full feature parity and explicit UI support for the OpenAI Codex backend (`chatgpt.com/backend-api/codex/responses`). The codebase is now entirely backend-agnostic and smoothly transitions between Anthropic Claude and OpenAI Codex schemas based on current authentication, without losing features like reasoning animations, token billing, or multi-modal visual inputs. + +## Key Changes + +### 1. Codex API Gateway Adapter (`codex-fetch-adapter.ts`) +- **Native Vision Translation**: Anthropic `base64` image schemas now map precisely to the Codex expected `input_image` payloads. +- **Strict Payload Mapping**: Refactored the internal mapping logic to translate `msg.content` items precisely into `input_text`, sidestepping OpenAI's strict `v1/responses` validation rules (`Invalid value: 'text'`). +- **Tool Logic Fixes**: Properly routed `tool_result` items into top-level `function_call_output` objects to guarantee that local CLI tool executions (File Reads, Bash loops) cleanly feed back into Codex logic without throwing "No tool output found" errors. +- **Cache Stripping**: Cleanly stripped Anthropic-only `cache_control` annotations from tool bindings and prompts prior to transmission so the Codex API doesn't reject malformed JSON. + +### 2. Deep UI & Routing Integration +- **Model Cleanups (`model.ts`)**: Updated `getPublicModelDisplayName` and `getClaudeAiUserDefaultModelDescription` to recognize Codex GPT strings. Models like `gpt-5.1-codex-max` now beautifully map to `Codex 5.1 Max` in the CLI visual outputs instead of passing the raw proxy IDs. +- **Default Reroutes**: Made `getDefaultMainLoopModelSetting` aware of `isCodexSubscriber()`, automatically defaulting to `gpt-5.2-codex` instead of `sonnet46`. +- **Billing Visuals (`logoV2Utils.ts`)**: Refactored `formatModelAndBilling` logic to render `Codex API Billing` proudly inside the terminal header when authenticated. + +### 3. Reasoning & Metrics Support +- **Thinking Animations**: `codex-fetch-adapter` now intentionally intercepts the proprietary `response.reasoning.delta` SSE frames emitted by `codex-max` models. It wraps them into Anthropic `` events, ensuring the standard CLI "Thinking..." spinner continues to function flawlessly for OpenAI reasoning. +- **Token Accuracy**: Bound logic to track `response.completed` completion events, fetching `usage.input_tokens` and `output_tokens`. These are injected natively into the final `message_stop` token handler, meaning Codex queries correctly trigger the terminal's Token/Price tracker summary logic. + +### 4. Git Housekeeping +- Configured `.gitignore` to securely and durably exclude the `openclaw/` gateway directory from staging commits. diff --git a/src/cli/handlers/auth.ts b/src/cli/handlers/auth.ts index c4cba5d06..15b0bb4f5 100644 --- a/src/cli/handlers/auth.ts +++ b/src/cli/handlers/auth.ts @@ -27,6 +27,7 @@ import { getOauthAccountInfo, getSubscriptionType, isUsing3PServices, + saveCodexOAuthTokens, saveOAuthTokensIfNeeded, validateForceLoginOrg, } from '../../utils/auth.js' @@ -43,6 +44,16 @@ import { buildAPIProviderProperties, } from '../../utils/status.js' +/** + * Returns true if the token carries any Anthropic-issued scope (user:* or org:*). + * Codex tokens use OpenID Connect scopes (openid, profile, email, offline_access) + * which are not Anthropic scopes, so this returns false for them. + */ +function hasAnyAnthropicScope(scopes: string[] | undefined): boolean { + if (!scopes?.length) return false + return scopes.some((s) => s.startsWith('user:') || s.startsWith('org:')) +} + /** * Shared post-token-acquisition logic. Saves tokens, fetches profile/roles, * and sets up the local auth state. @@ -96,7 +107,7 @@ export async function installOAuthTokens(tokens: OAuthTokens): Promise { await fetchAndStoreClaudeCodeFirstTokenDate().catch(err => logForDebugging(String(err), { level: 'error' }), ) - } else { + } else if (hasAnyAnthropicScope(tokens.scopes)) { // API key creation is critical for Console users — let it throw. const apiKey = await createAndStoreApiKey(tokens.accessToken) if (!apiKey) { @@ -104,6 +115,16 @@ export async function installOAuthTokens(tokens: OAuthTokens): Promise { 'Unable to create API key. The server accepted the request but did not return a key.', ) } + } else { + // Third-party provider (e.g. OpenAI Codex) — tokens carry no Anthropic + // scopes. Skip Anthropic API key creation entirely and store the tokens + // in their own dedicated config slot. + saveCodexOAuthTokens({ + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken ?? '', + expiresAt: tokens.expiresAt ?? Date.now() + 3600_000, + accountId: (tokens.tokenAccount?.uuid ?? ''), + }) } await clearAuthRelatedCaches() diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 717697f68..abbae9228 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -9,8 +9,9 @@ import { Box, Link, Text } from '../ink.js'; import { useKeybinding } from '../keybindings/useKeybinding.js'; import { getSSLErrorHint } from '../services/api/errorUtils.js'; import { sendNotification } from '../services/notifier.js'; +import { runCodexOAuthFlow } from '../services/oauth/codex-client.js'; import { OAuthService } from '../services/oauth/index.js'; -import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js'; +import { getOauthAccountInfo, saveCodexOAuthTokens, validateForceLoginOrg } from '../utils/auth.js'; import { logError } from '../utils/log.js'; import { getSettings_DEPRECATED } from '../utils/settings/settings.js'; import { Select } from './CustomSelect/select.js'; @@ -84,6 +85,7 @@ export function ConsoleOAuthFlow({ // Use Claude AI auth for setup-token mode to support user:inference scope return mode === 'setup-token' || forceLoginMethod === 'claudeai'; }); + const [loginWithCodex, setLoginWithCodex] = useState(false); // After a few seconds we suggest the user to copy/paste url if the // browser did not open automatically. In this flow we expect the user to // copy the code from the browser and paste it in the terminal @@ -235,7 +237,7 @@ export function ConsoleOAuthFlow({ await installOAuthTokens(result); const orgResult = await validateForceLoginOrg(); if (!orgResult.valid) { - throw new Error(orgResult.message); + throw new Error('message' in orgResult ? (orgResult as any).message : 'Invalid organization'); } setOAuthStatus({ state: 'success' @@ -261,16 +263,46 @@ export function ConsoleOAuthFlow({ }); } }, [oauthService, setShowPastePrompt, loginWithClaudeAi, mode, orgUUID]); + + // Codex-specific OAuth flow — completely separate from the Anthropic OAuthService + const startCodexOAuth = useCallback(async () => { + try { + logEvent('tengu_oauth_codex_flow_start', {}); + const codexTokens = await runCodexOAuthFlow(async (url) => { + setOAuthStatus({ state: 'waiting_for_login', url }); + setTimeout(setShowPastePrompt, 3000, true); + }); + // Save directly via saveCodexOAuthTokens (bypasses installOAuthTokens Anthropic path) + saveCodexOAuthTokens(codexTokens); + logEvent('tengu_oauth_codex_success', {}); + setOAuthStatus({ state: 'success' }); + void sendNotification({ message: 'Codex login successful', notificationType: 'auth_success' }, terminal); + } catch (err) { + const msg = (err as Error).message; + logEvent('tengu_oauth_codex_error', { + error: msg as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }); + setOAuthStatus({ state: 'error', message: msg, toRetry: { state: 'idle' } }); + } + }, [setShowPastePrompt, terminal]); + const pendingOAuthStartRef = useRef(false); useEffect(() => { if (oauthStatus.state === 'ready_to_start' && !pendingOAuthStartRef.current) { pendingOAuthStartRef.current = true; - process.nextTick((startOAuth_0: () => Promise, pendingOAuthStartRef_0: React.MutableRefObject) => { - void startOAuth_0(); - pendingOAuthStartRef_0.current = false; - }, startOAuth, pendingOAuthStartRef); + if (loginWithCodex) { + process.nextTick((startCodexOAuth_0: () => Promise, pendingOAuthStartRef_0: React.MutableRefObject) => { + void startCodexOAuth_0(); + pendingOAuthStartRef_0.current = false; + }, startCodexOAuth, pendingOAuthStartRef); + } else { + process.nextTick((startOAuth_0: () => Promise, pendingOAuthStartRef_0: React.MutableRefObject) => { + void startOAuth_0(); + pendingOAuthStartRef_0.current = false; + }, startOAuth, pendingOAuthStartRef); + } } - }, [oauthStatus.state, startOAuth]); + }, [oauthStatus.state, startOAuth, startCodexOAuth, loginWithCodex]); // Auto-exit for setup-token mode useEffect(() => { @@ -325,7 +357,7 @@ export function ConsoleOAuthFlow({ } - + ; } @@ -343,9 +375,10 @@ type OAuthStatusMessageProps = { handleSubmitCode: (value: string, url: string) => void; setOAuthStatus: (status: OAuthStatus) => void; setLoginWithClaudeAi: (value: boolean) => void; + setLoginWithCodex: (value: boolean) => void; }; function OAuthStatusMessage(t0) { - const $ = _c(51); + const $ = _c(52); const { oauthStatus, mode, @@ -359,7 +392,8 @@ function OAuthStatusMessage(t0) { textInputColumns, handleSubmitCode, setOAuthStatus, - setLoginWithClaudeAi + setLoginWithClaudeAi, + setLoginWithCodex } = t0; switch (oauthStatus.state) { case "idle": @@ -405,20 +439,29 @@ function OAuthStatusMessage(t0) { t6 = [t4, t5, { label: 3rd-party platform ·{" "}Amazon Bedrock, Microsoft Foundry, or Vertex AI{"\n"}, value: "platform" + }, { + label: OpenAI Codex account ·{" "}ChatGPT Plus/Pro subscription{"\n"}, + value: "codex" }]; $[5] = t6; } else { t6 = $[5]; } let t7; - if ($[6] !== setLoginWithClaudeAi || $[7] !== setOAuthStatus) { + if ($[6] !== setLoginWithClaudeAi || $[7] !== setOAuthStatus || $[8] !== setLoginWithCodex) { t7 =