Skip to content

feat: AI provider preset system + profile UX + Codex fixes#112

Merged
luokerenx4 merged 18 commits intomasterfrom
dev
Apr 8, 2026
Merged

feat: AI provider preset system + profile UX + Codex fixes#112
luokerenx4 merged 18 commits intomasterfrom
dev

Conversation

@luokerenx4
Copy link
Copy Markdown
Contributor

Summary

Major overhaul of the AI provider configuration system, building on the Codex integration and profile system from PR #106.

Preset System

  • Schema-driven presets — each preset is a Zod schema that generates JSON Schema for the frontend. Adding a new provider = editing preset-catalog.ts only
  • Built-in presets: Claude (OAuth/API), OpenAI Codex (OAuth/API), Gemini, MiniMax, Custom
  • Preset fields enforce constraints: const = locked, oneOf = dropdown with labels, writeOnly = password
  • useSchemaForm hook — generic JSON Schema → form renderer, no hardcoded field logic

Profile UX

  • Modal-based UI — clean profile list + edit/create modals (replaces inline forms)
  • Name as identifierlabel removed, profile key = display name
  • Official preset names locked — can't rename, can't create duplicates
  • Connection test before save — "Test Connection" → shows AI response → "Save" to confirm
  • Global API keys removed — each profile carries its own key

Codex Provider Fixes

  • store: false required by ChatGPT subscription endpoint
  • input must be array (not string)
  • response.output_item.done for function call call_id (not arguments.done)
  • ask() uses streaming (non-streaming returns 400 on subscription endpoint)
  • Structured multi-turn input via toResponsesInput()

Agent SDK Fixes

  • CLAUDE_CODE_SIMPLE=1 forces API key mode, prevents OAuth fallback when local login exists
  • ask() now throws on ok: false instead of returning error text as success
  • ask(prompt, profile?) — all providers accept profile parameter for correct auth

E2E Tests

  • Codex provider e2e: basic text, tool call round-trip, multi-turn context

Test plan

  • pnpm test — 974 tests passing
  • pnpm build — clean
  • Codex e2e tests pass (4/4)
  • UI: create profiles from presets, test connection, edit, delete
  • Wrong API key → test fails (red), correct key → test passes (green) → save

🤖 Generated with Claude Code

Ame and others added 18 commits April 8, 2026 11:42
Verifies real API communication against the ChatGPT subscription endpoint:
- Basic text response
- Tool call with correct call_id/name/arguments (via output_item.done)
- Full tool call round-trip (send result back, get final text)
- Multi-turn structured input (model references prior context)

Skips gracefully if ~/.codex/auth.json is not present.
Runs via `pnpm test:e2e`, excluded from regular `pnpm test`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Presets are constraint-based templates that define which fields are
locked/hidden/required and what models are available to choose from.
Users pick a preset, then fill only the editable fields.

Built-in presets:
- Claude (OAuth subscription / API key)
- OpenAI/Codex (ChatGPT subscription / API key)
- Google Gemini
- MiniMax (third-party, Anthropic-compatible API)
- Custom (full control)

Also fixes OAuth model handling: when loginMethod is 'claudeai',
model is optional — omitting it lets Claude Code pick based on the
user's subscription plan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UX redesign:
- Main page is a clean profile list with "Set Default" / "Edit" buttons
- Edit opens a modal (preset-aware form with locked/hidden fields)
- New Profile opens a modal (step 1: choose preset, step 2: fill form)
- No more inline forms or mixed state between list and editor

Remove global API keys:
- apiKeys field migrated into individual profile.apiKey fields
- resolveProfile() no longer falls back to global keys
- Removed PUT /api-keys and GET /api-keys/status endpoints
- Removed frontend ApiKeysForm and related API methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Presets now generate JSON Schema from Zod definitions. Frontend renders
forms dynamically from schema — no hardcoded field logic.

Backend (presets.ts):
- Each preset defined as a Zod schema + metadata (label, description, hint)
- z.toJSONSchema() serializes to JSON Schema, post-processed for:
  - oneOf + const + title for model dropdowns with labels
  - writeOnly for password fields (apiKey)
- z.literal() → const (hidden, value baked in)

Frontend (AIProviderPage.tsx):
- SchemaForm component: generic JSON Schema → form renderer
  - const → hidden, oneOf → labeled dropdown, writeOnly → password,
    enum → dropdown, string → text input
  - required/default/description from schema
- Removes PresetFields, PresetField, PresetModelOption types
- Preset hint renders as guidance text above form

Adding a new provider preset only requires editing presets.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Profile objects no longer have a `label` field — the profiles Record
key (name) serves as both unique identifier and display name.

- Remove label from profile schemas, ResolvedProfile, frontend types
- Presets gain defaultName — official presets auto-fill the name field,
  users only need to fill name for Custom presets
- Frontend displays profile key directly in list and modal titles

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MiniMax's Anthropic-compatible API works with Claude Agent SDK, not
Vercel AI SDK (which appends /messages and gets 404).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add currency tracking throughout the UTA abstraction layer to fix
incorrect display of non-USD positions (e.g. HKD stocks shown as USD).

- Add `currency` field to Position, `baseCurrency` to AccountInfo
- New FxService with dual-table design: hardcoded default rates for
  offline resilience + live cache from market-data currency client
- Lookup priority: live → stale cache → default table → 1:1 fallback
- All four brokers (IBKR, Alpaca, CCXT, Mock) populate currency fields
- CCXT normalizes stablecoins (USDT/USDC/BUSD) to USD at broker layer
- AccountManager aggregates equity with FX conversion to USD
- Snapshots and trading tools carry currency information through
- 15 new tests for FxService covering full degradation chain

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…link

- preset-catalog.ts: pure data file with Zod-defined preset declarations.
  Update model versions by editing only this file.
- presets.ts: slimmed to pure serialization (Zod → JSON Schema + post-processing)
- Profile schema gains `preset` field — links profile to its source preset.
  Edit modal looks up preset by profile.preset instead of reverse-matching.
- useSchemaForm hook: parses JSON Schema into field descriptors + form state.
  Components just render fields by type, no schema parsing logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- useSchemaForm: reset formData when schema changes (fixes "Model is
  required" when selecting a preset in create modal)
- Remove select-custom field type — oneOf fields render as strict
  dropdowns without "Custom..." option. Official preset model lists
  are enforced, not escapable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove modelOptional and "Auto" option — OAuth presets now default to
a specific model (claude-sonnet-4-6, gpt-5.4) instead of allowing
empty. Simplifies validation and avoids the "Model is required" error
when users select Auto.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n test

- Official preset names are read-only (locked to defaultName)
- Already-configured presets show "Already configured" and are disabled
- "Create & Test" button: saves profile then sends "Hi" to verify
  connectivity, displays response or error
- New POST /config/profiles/:slug/test endpoint via AgentCenter.testProfile()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
handleCreateSave no longer closes modal — the modal manages its own
lifecycle: save → test → show result → auto-close on success after 2s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Profile names with "/" break URL routing (Hono treats it as path
separator). Fixed by:
- Renaming "OpenAI / Codex" to "OpenAI Codex" in preset defaults
- Adding encodeURIComponent() to all profile API URL paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…treaming

The ChatGPT subscription endpoint returns 400 for non-streaming
responses.create() calls. Changed ask() to use responses.stream()
matching the same pattern as generate().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AIProvider.ask() now takes optional ResolvedProfile — providers use it
for auth/model/endpoint instead of falling back to defaults. Fixes Codex
ask() always using OAuth regardless of profile loginMethod.

GenerateRouter gains askWithProfile() for inline (unsaved) profiles.

Test endpoint changed to POST /profiles/test — accepts profile data,
tests connectivity WITHOUT saving first. Frontend flow:
  validate → test (with profile data) → show result → save on success

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test Connection button runs the test. On success, button changes to
Save for user to explicitly confirm. No more auto-save after test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…llback

When loginMethod is 'api-key', the Claude Code CLI would silently
use a local OAuth session from ~/.claude/ instead of the provided
ANTHROPIC_API_KEY. Setting CLAUDE_CODE_SIMPLE=1 (equivalent to --bare)
disables OAuth entirely, forcing the CLI to use the API key.

Investigated via Claude Code source: forceLoginMethod only controls
which OAuth provider to use, not API key vs OAuth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When askAgentSdk returns ok:false (e.g. CLI exit code 1 from invalid
API key), ask() was returning the error message as successful text.
Now it throws, so the test endpoint correctly reports failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@luokerenx4 luokerenx4 merged commit f44c878 into master Apr 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant