Skip to content

Foxl v0.2.19

Choose a tag to compare

@foxl-bot foxl-bot released this 24 Apr 11:59

generate_image tool: gpt-image-2 through ChatGPT subscription

  • New built-in generate_image tool that produces or edits raster images via gpt-image-2 without an OpenAI API key. Powered entirely by the user's existing ChatGPT Plus/Pro OAuth session (~/.codex/auth.json / CODEX_HOME / CHATGPT_LOCAL_HOME); no per-image billing outside the $20/month subscription quota. The tool posts to chatgpt.com/backend-api/codex/responses with model: "gpt-5.4" carrying tools: [{ type: "image_generation", model: "gpt-image-2" }] — the same shape the official Codex CLI uses for its built-in image_gen tool.
  • Tool is gated to accounts with ChatGPT OAuth: only registered when the credential file is discoverable at module load, skipped otherwise so non-OAuth users don't see an unusable affordance.
  • Accepts multi-image input for edits and composition. inputImages: string[] (max 10 paths) switches gpt-image-2 from text-to-image into edit / compose mode — "이 사진 만화풍으로 바꿔", "이 두 이미지 합쳐줘", reference-guided generation, etc.
  • Token-efficient wire shape: only file paths cross the model's tool-call args, never base64. The tool reads each path from disk inside the server process and encodes it as input_image data URIs directly on the Codex request. Return payload to the model is trimmed to Saved to: <path> + a sharp-resized 768px JPEG preview (~100KB) — the full-resolution PNG stays on disk at outPath.
  • Attached chat images auto-persist to data/workspace/attachments/chat-<timestamp>-<rand>.<ext> and their absolute paths are injected into the user's text block as [Attached image paths: "..."]. The model therefore always knows the exact disk path to pass to generate_image({ inputImages: [...] }), closing the loop from drag-and-drop → edit.
  • Saved outputs live at data/workspace/generated/generated-<YYYY-MM-DD>-<prompt-slug>-<suffix>.png. Workspace root (SOUL/USER/MEMORY/TOOLS/AGENTS/HEARTBEAT/BOOTSTRAP.md) stays uncluttered; every generated image carries its date and a readable slug derived from the first six words of the prompt.
  • data/workspace/generated/ excluded from workspace file listing automatically (subfolder convention), accessible via the Open Folder button now on the Workspace page header.
  • ChatGPT-style UI: generated images render expanded by default directly in the chat stream (not tucked inside a collapsible tool flip), with the standard Task chevron for collapse and consistent sizing with other tool calls. view_image keeps its original collapsed-by-default compact UX. The full-resolution preview modal offers click-to-zoom (25%–400%) and an Open Folder button that reveals the saved PNG in Finder / Explorer / xdg-open.
  • Tool-profile wiring: added media category (['generate_image']) to tool-profiles.ts and included it in default, standard, and full profiles. minimal omits it to preserve its token budget.

OpenAI OAuth: catalog + model availability

  • Added openai-oauth/gpt-5.5 to the catalog (1M context, reasoning + streaming). Verified serving on the ChatGPT OAuth endpoint via live probe. gpt-5.5-codex, gpt-5.5-mini, gpt-5.5-pro are all gated to API-key accounts (400 Bad Request: "not supported when using Codex with a ChatGPT account") and therefore excluded.
  • Final catalog: gpt-5.5 (1M), gpt-5.4 (1M), gpt-5.4-mini (400K), gpt-5.3-codex (400K).
  • Removed openai-oauth/gpt-5.4-codex and openai-oauth/gpt-image-2 as top-level language-model entries. The ChatGPT OAuth backend returns 400 Bad Request: "The 'gpt-5.4-codex' model is not supported when using Codex with a ChatGPT account" (same for gpt-image-2) despite the OpenAI Developer Community announcement. gpt-image-2 is still reachable, but through the new generate_image tool above.
  • Wired up imageModel() / image() on createOpenAIOAuth regardless, backed by @ai-sdk/openai/internal OpenAIImageModel reused through the same OAuth fetch. Ready to flip on the moment OpenAI enables raw image models on the ChatGPT OAuth endpoint.

Fix: openai-oauth/gpt-5.4 was silently routed to gpt-4.1

  • shared/model-resolver.ts had a stale NAMESPACE_ALIASES entry mapping openai-oauth/gpt-5.4gpt-4.1 (and openai-oauth/gpt-5.4-codexgpt-4.1). Any time the user selected the OAuth GPT-5.4, setModel → toCanonicalId resolved the alias and stored gpt-4.1 as the active model — which Codex then rejected with a bare 400 Bad Request because ChatGPT OAuth accounts don't serve gpt-4.1 either. Removed both aliases so the transport-prefixed id passes through untouched. Verified end-to-end: /api/providers/select now persists openai-oauth/gpt-5.4 verbatim and the follow-up /api/chat/stream returns reasoning + text deltas as expected.

Debug: surface real Codex upstream errors behind FOXL_CODEX_DEBUG=1

  • Added an opt-in FOXL_CODEX_DEBUG=1 env flag in openai-oauth/core/transport.ts that clones and logs the response body + request body on any non-2xx from chatgpt.com/backend-api/codex/*. Strands wraps upstream errors as "Language model stream error: Bad Request", which discards the real cause; this flag is the single switch that reveals it. Off by default (no logging overhead in production).

UX: Workspace page "Open Folder" button

  • New folder-open icon button in the Workspace header (next to Search and Refresh) that opens data/workspace/ in Finder on macOS, Explorer on Windows, and xdg-open on Linux. Electron shell path via window.pilot.openPath; non-Electron falls back to new POST /api/system/open endpoint.

Skills: remove duplicate image-gen skills from foxl-ai/skills

  • Removed openai-image-gen (OPENAI_API_KEY + Python script path) and nano-banana-pro (GEMINI_API_KEY + uv + Python) from the skills repo. Both are superseded by the native generate_image tool above. One canonical image path across the product.

Dependencies: Strands SDK → v1.0.0-rc.5

  • Bumped @strands-agents/sdk from ^1.0.0-rc.3 to ^1.0.0-rc.5. Picks up: mid-execution cancellation (agent.cancel() / cancellationSignal via AbortSignal.timeout()), agent-as-tool (agent.asTool()), multi-agent session persistence, Bedrock thinking+forced tool_choice conflict fix, Bedrock context window overflow detection matching the Python SDK, invocation lock leak fix on consumer stream break, AgentSkillsPlugin → AgentSkills rename, contextWindowLimit added to BaseModelConfig, and OTEL JS SDK v2 upgrade. No call-site changes needed in Foxl.

Frontend hygiene: zero web typecheck errors

  • Fixed 19 pre-existing TypeScript errors across the web codebase so tsc --noEmit in apps/web is now green. Bumped target/lib from ES2020 → ES2023 and added vite/client to the types array (unlocks Array.prototype.findLastIndex, stricter ArrayBuffer typing, and import.meta.env typing). Deleted unused FloatingLines.tsx (referenced a missing three dep). Tightened cast sites in useWebSocket.ts (WebSocketMessage had an [key: string]: unknown index signature that made field narrowing impossible), tunnel-crypto.ts (BufferSource / Uint8Array overloads), and search-bar.tsx (RefObject nullability). None of these were blocking the Vite build, but keeping tsc green is a non-negotiable guardrail for future refactors.

Claude Code (OAuth): Opus 4.7 gets its 1M context back

  • Opus 4.7 on the Claude Code OAuth transport now reports its true 1M context window. Since Opus 4.7 shipped in v0.2.11, claude-code/opus had been hard-coded to 200K because the CC OAuth catalog predated the 1M rollout. The result: /context usage percentages showed ~5× inflated values and auto-compaction kicked in five times earlier than it should have, effectively capping Opus 4.7 sessions at 200K. A live probe against api.anthropic.com with a 320K-token prompt confirmed Opus 4.7 on the subscription pool accepts the full input at service_tier: standard with no context-1m-* beta header — 1M is the native default, the beta flag is only for back-compat with older models.
  • Sonnet 4.6 on the same CC OAuth transport stays at 200K. A parallel probe (identical shape, only model swapped) returned HTTP 429 "Extra usage is required for long context requests" — Anthropic routes Sonnet's long-context requests into the pay-as-you-go pool instead of the subscription pool. Haiku 4.5 is likewise kept at 200K.
  • Added resolveEffectiveContextWindow(modelId) in shared/model-resolver.ts so UI context gauges (useStreamingChat.ts) and server-side auto-compaction (server/agent/compaction.ts) both see the transport-adjusted ceiling rather than blindly trusting the catalog entry. Catalog entries keep their paper-spec numbers (Sonnet 4.6 = 1M via Bedrock/BYOK) and the helper narrows them only for transports that impose a cap.

Claude Code (OAuth) model ids: full Anthropic name after the prefix

  • The CC OAuth catalog now exposes claude-code/claude-opus-4-7 and claude-code/claude-sonnet-4-6 as the canonical ids. The legacy short-form ids (claude-code/opus, claude-code/sonnet, claude-code/haiku) are preserved as aliases in CLAUDE_CODE_MODEL_ALIASES and in the shared NAMESPACE_ALIASES map, so previously persisted user selections (localStorage foxl-selected-model, relay /api/providers/select state) keep resolving correctly. Verified end-to-end with a live probe — all four id shapes (new/old × Opus/Sonnet) route to the expected Anthropic model with service_tier: standard.

Fix: model switch was a no-op on foxl.ai provider

  • Switching model mid-conversation on the foxl.ai provider (e.g. Kimi K2.5 → Opus 4.7) kept running the previous model because conversationAgents caches Strands Agent instances and Agent is constructed with a baked-in model + provider. The POST /api/chat/stream handler already called setModel(newModel) from the request body, but the cached agent ignored it. Added a parallel conversationAgentModels map that records each cached agent's construction-time model, and handleStreamingChat() now discards the cached agent as soon as cachedModel !== config.model. All 16 existing conversationAgents.delete(…) / .clear() sites were mirrored so the maps never drift.

Relay: prompt caching on the Bedrock forward path

  • relay/src/proxy.ts now injects block-level cache_control: { type: 'ephemeral' } breakpoints on every Anthropic-format /v1/messages body before it hands off to Bedrock InvokeModel. The tool list, the system prompt, and the last user turn each get one breakpoint (up to Bedrock's 4-breakpoint limit) so tools → system → messages prefix caching kicks in automatically across turns. Bodies that already carry explicit cache_control markers are left alone. Verified with a dedicated unit-test suite (relay/tests/09-cache-breakpoints.test.ts, 7 cases covering string/array system shapes, tool lists, multi-turn conversations, and tool_result blocks).
  • Strands' AnthropicModel config only exposes a cacheConfig that BedrockModel honors — AnthropicModel itself doesn't auto-inject cache points, so the relay-side injection is where caching on the foxl relay path actually turns on. Bedrock InvokeModel does not honor the top-level automatic-caching shorthand described in Anthropic's API docs (that's Claude API + Azure AI Foundry only), which is why block-level markers are the right place to hook.

Site: foxl.ai gets a real dark mode

  • Added a .dark palette to site/landing/src/index.css (background/foreground/card/muted/border/ring/accent all defined) and an inline pre-paint script in index.html that applies the saved or system theme before first paint so there's no FOUC on hard refresh. A new ThemeToggle component (site/landing/src/components/landing/ThemeToggle.tsx) sits in the Navbar next to the Download button on desktop and next to the hamburger on mobile, persisting the user's choice under localStorage['foxl-theme'] and falling back to prefers-color-scheme when unset.
  • Swept landing components for hardcoded light palette: Security.tsx cards switched from bg-white to the semantic bg-card, and the LegalPage changelog version badge switched from bg-zinc-200 text-zinc-700 to bg-muted text-muted-foreground. The only remaining hardcoded colors are intentional (the iPhone notch in MobileDemo.tsx stays bg-black, the toggle knob in DesktopDemo.tsx stays bg-white).