Repository: glowbom/glowby
Branch: main
Commit: 84215fc03b8b
Files: 339
Total size: 7.4 MB
Directory structure:
gitextract_gwqak2n4/
├── .claude/
│ └── settings.json
├── .github/
│ └── workflows/
│ └── glowby-release.yml
├── .gitignore
├── AGENTS.md
├── CLI.md
├── LICENSE
├── README.md
├── backend/
│ ├── backend_info_page.go
│ ├── claude_anthropic.go
│ ├── codex_chatgpt.go
│ ├── elevenlabs_audio.go
│ ├── fireworks_fireworks.go
│ ├── gemini_google.go
│ ├── gemini_image.go
│ ├── gemini_text_with_attachment.go
│ ├── gemini_veo.go
│ ├── gemini_video.go
│ ├── go.mod
│ ├── go.sum
│ ├── gpt5_openai.go
│ ├── gpt5_responses_api.go
│ ├── grok_image.go
│ ├── grok_video.go
│ ├── grok_xai.go
│ ├── handlers.go
│ ├── main.go
│ ├── model_helpers.go
│ ├── o3_openai.go
│ ├── openai_image.go
│ ├── opencode_driver.go
│ ├── opencode_folder_picker.go
│ ├── opencode_media_postpass.go
│ ├── opencode_project_history.go
│ ├── opencode_project_open.go
│ ├── opencode_zen.go
│ ├── openrouter_openrouter.go
│ ├── prompts.go
│ ├── r1_groq.go
│ ├── r1_ollama.go
│ ├── search.go
│ ├── security.go
│ ├── stack_specs.go
│ └── types.go
├── cli/
│ ├── code.go
│ ├── doctor.go
│ ├── go.mod
│ ├── main.go
│ └── version.go
├── docs/
│ ├── .dockerignore
│ ├── .gitignore
│ ├── Dockerfile
│ ├── README.md
│ ├── app/
│ │ ├── app.css
│ │ ├── components/
│ │ │ ├── brand-title.tsx
│ │ │ ├── docs-route-view.tsx
│ │ │ ├── docs-search.tsx
│ │ │ └── nav-actions.tsx
│ │ ├── lib/
│ │ │ ├── docs.ts
│ │ │ ├── meta.ts
│ │ │ ├── site.ts
│ │ │ ├── source.browser.tsx
│ │ │ └── source.server.ts
│ │ ├── root.tsx
│ │ ├── routes/
│ │ │ ├── docs-page.tsx
│ │ │ └── home.tsx
│ │ └── routes.ts
│ ├── content/
│ │ └── docs/
│ │ ├── desktop.mdx
│ │ ├── glowbom.mdx
│ │ ├── glowby-oss.mdx
│ │ ├── index.mdx
│ │ ├── meta.json
│ │ └── quickstart.mdx
│ ├── package.json
│ ├── react-router.config.ts
│ ├── server.log
│ ├── source.config.ts
│ ├── test-browser.js
│ ├── test-search.js
│ ├── test.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── legacy/
│ ├── GlowbyGenius.md
│ ├── README.md
│ ├── app/
│ │ ├── .gitignore
│ │ ├── .metadata
│ │ ├── .vscode/
│ │ │ └── launch.json
│ │ ├── README.md
│ │ ├── analysis_options.yaml
│ │ ├── android/
│ │ │ ├── .gitignore
│ │ │ ├── app/
│ │ │ │ ├── build.gradle
│ │ │ │ └── src/
│ │ │ │ ├── debug/
│ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ ├── main/
│ │ │ │ │ ├── AndroidManifest.xml
│ │ │ │ │ ├── kotlin/
│ │ │ │ │ │ └── com/
│ │ │ │ │ │ └── example/
│ │ │ │ │ │ └── pwa/
│ │ │ │ │ │ └── MainActivity.kt
│ │ │ │ │ └── res/
│ │ │ │ │ ├── drawable/
│ │ │ │ │ │ └── launch_background.xml
│ │ │ │ │ └── values/
│ │ │ │ │ └── styles.xml
│ │ │ │ └── profile/
│ │ │ │ └── AndroidManifest.xml
│ │ │ ├── build.gradle
│ │ │ ├── gradle/
│ │ │ │ └── wrapper/
│ │ │ │ └── gradle-wrapper.properties
│ │ │ ├── gradle.properties
│ │ │ └── settings.gradle
│ │ ├── assets/
│ │ │ ├── talk.glowbom
│ │ │ ├── test.html
│ │ │ └── website.html
│ │ ├── ios/
│ │ │ ├── .gitignore
│ │ │ ├── Flutter/
│ │ │ │ ├── .last_build_id
│ │ │ │ ├── AppFrameworkInfo.plist
│ │ │ │ ├── Debug.xcconfig
│ │ │ │ └── Release.xcconfig
│ │ │ ├── Podfile
│ │ │ ├── Runner/
│ │ │ │ ├── AppDelegate.swift
│ │ │ │ ├── Assets.xcassets/
│ │ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ │ └── Contents.json
│ │ │ │ │ └── LaunchImage.imageset/
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ └── README.md
│ │ │ │ ├── Base.lproj/
│ │ │ │ │ ├── LaunchScreen.storyboard
│ │ │ │ │ └── Main.storyboard
│ │ │ │ ├── Glowby.swift
│ │ │ │ ├── Info.plist
│ │ │ │ └── Runner-Bridging-Header.h
│ │ │ ├── Runner.xcodeproj/
│ │ │ │ ├── project.pbxproj
│ │ │ │ ├── project.xcworkspace/
│ │ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ │ └── xcshareddata/
│ │ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ │ └── WorkspaceSettings.xcsettings
│ │ │ │ └── xcshareddata/
│ │ │ │ └── xcschemes/
│ │ │ │ └── Runner.xcscheme
│ │ │ └── Runner.xcworkspace/
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata/
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ │ ├── lib/
│ │ │ ├── main.dart
│ │ │ ├── models/
│ │ │ │ └── ai.dart
│ │ │ ├── services/
│ │ │ │ ├── hugging_face_api.dart
│ │ │ │ ├── openai_api.dart
│ │ │ │ └── pulze_ai_api.dart
│ │ │ ├── utils/
│ │ │ │ ├── color_utils.dart
│ │ │ │ ├── text_to_speech.dart
│ │ │ │ ├── timestamp.dart
│ │ │ │ ├── utils.dart
│ │ │ │ ├── utils_desktop.dart
│ │ │ │ ├── utils_stub.dart
│ │ │ │ └── utils_web.dart
│ │ │ └── views/
│ │ │ ├── dialogs/
│ │ │ │ ├── ai_error_dialog.dart
│ │ │ │ ├── ai_settings_dialog.dart
│ │ │ │ └── api_key_dialog.dart
│ │ │ ├── html/
│ │ │ │ ├── html_view_screen.dart
│ │ │ │ ├── html_view_screen_desktop.dart
│ │ │ │ ├── html_view_screen_interface.dart
│ │ │ │ ├── html_view_screen_mobile.dart
│ │ │ │ ├── html_view_screen_stub.dart
│ │ │ │ └── html_view_screen_web.dart
│ │ │ ├── screens/
│ │ │ │ ├── chat_screen.dart
│ │ │ │ ├── global_settings.dart
│ │ │ │ ├── magical_loading_view.dart
│ │ │ │ └── talk_screen.dart
│ │ │ └── widgets/
│ │ │ ├── message.dart
│ │ │ ├── message_bubble.dart
│ │ │ ├── messages.dart
│ │ │ ├── new_message.dart
│ │ │ ├── paint_window.dart
│ │ │ └── tasks_view.dart
│ │ ├── linux/
│ │ │ ├── .gitignore
│ │ │ ├── CMakeLists.txt
│ │ │ ├── flutter/
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── generated_plugin_registrant.cc
│ │ │ │ ├── generated_plugin_registrant.h
│ │ │ │ └── generated_plugins.cmake
│ │ │ ├── main.cc
│ │ │ ├── my_application.cc
│ │ │ └── my_application.h
│ │ ├── macos/
│ │ │ ├── .gitignore
│ │ │ ├── Flutter/
│ │ │ │ ├── Flutter-Debug.xcconfig
│ │ │ │ ├── Flutter-Release.xcconfig
│ │ │ │ └── GeneratedPluginRegistrant.swift
│ │ │ ├── Podfile
│ │ │ ├── Runner/
│ │ │ │ ├── AppDelegate.swift
│ │ │ │ ├── Assets.xcassets/
│ │ │ │ │ └── AppIcon.appiconset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── Base.lproj/
│ │ │ │ │ └── MainMenu.xib
│ │ │ │ ├── Configs/
│ │ │ │ │ ├── AppInfo.xcconfig
│ │ │ │ │ ├── Debug.xcconfig
│ │ │ │ │ ├── Release.xcconfig
│ │ │ │ │ └── Warnings.xcconfig
│ │ │ │ ├── DebugProfile.entitlements
│ │ │ │ ├── Info.plist
│ │ │ │ ├── MainFlutterWindow.swift
│ │ │ │ └── Release.entitlements
│ │ │ ├── Runner.xcodeproj/
│ │ │ │ ├── project.pbxproj
│ │ │ │ ├── project.xcworkspace/
│ │ │ │ │ └── xcshareddata/
│ │ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ │ └── xcshareddata/
│ │ │ │ └── xcschemes/
│ │ │ │ └── Runner.xcscheme
│ │ │ ├── Runner.xcworkspace/
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata/
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ └── RunnerTests/
│ │ │ └── RunnerTests.swift
│ │ ├── pubspec.yaml
│ │ ├── web/
│ │ │ ├── index.html
│ │ │ ├── manifest.json
│ │ │ └── tv.js
│ │ └── windows/
│ │ ├── .gitignore
│ │ ├── CMakeLists.txt
│ │ ├── flutter/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── generated_plugin_registrant.cc
│ │ │ ├── generated_plugin_registrant.h
│ │ │ └── generated_plugins.cmake
│ │ └── runner/
│ │ ├── CMakeLists.txt
│ │ ├── Runner.rc
│ │ ├── flutter_window.cpp
│ │ ├── flutter_window.h
│ │ ├── main.cpp
│ │ ├── resource.h
│ │ ├── runner.exe.manifest
│ │ ├── utils.cpp
│ │ ├── utils.h
│ │ ├── win32_window.cpp
│ │ └── win32_window.h
│ ├── backend/
│ │ └── aws/
│ │ └── fetchImageAsBase64/
│ │ └── index.mjs
│ ├── dist/
│ │ ├── assets/
│ │ │ ├── AssetManifest.bin.json
│ │ │ ├── AssetManifest.json
│ │ │ ├── FontManifest.json
│ │ │ ├── NOTICES
│ │ │ ├── assets/
│ │ │ │ └── talk.glowbom
│ │ │ ├── fonts/
│ │ │ │ └── MaterialIcons-Regular.otf
│ │ │ └── shaders/
│ │ │ └── ink_sparkle.frag
│ │ ├── canvaskit/
│ │ │ ├── canvaskit.js
│ │ │ ├── canvaskit.js.symbols
│ │ │ ├── canvaskit.wasm
│ │ │ ├── chromium/
│ │ │ │ ├── canvaskit.js
│ │ │ │ ├── canvaskit.js.symbols
│ │ │ │ └── canvaskit.wasm
│ │ │ ├── skwasm.js
│ │ │ ├── skwasm.js.symbols
│ │ │ ├── skwasm.wasm
│ │ │ └── skwasm.worker.js
│ │ ├── flutter.js
│ │ ├── flutter_bootstrap.js
│ │ ├── flutter_service_worker.js
│ │ ├── index.html
│ │ ├── manifest.json
│ │ ├── tv.js
│ │ └── version.json
│ └── docs/
│ └── README.md
├── project/
│ ├── AGENTS.md
│ ├── android/
│ │ ├── .gitignore
│ │ ├── .idea/
│ │ │ ├── .gitignore
│ │ │ ├── .name
│ │ │ ├── compiler.xml
│ │ │ ├── deploymentTargetDropDown.xml
│ │ │ ├── gradle.xml
│ │ │ ├── inspectionProfiles/
│ │ │ │ └── Project_Default.xml
│ │ │ ├── kotlinc.xml
│ │ │ ├── migrations.xml
│ │ │ ├── misc.xml
│ │ │ └── vcs.xml
│ │ ├── README.md
│ │ ├── app/
│ │ │ ├── .gitignore
│ │ │ ├── build.gradle.kts
│ │ │ ├── proguard-rules.pro
│ │ │ └── src/
│ │ │ └── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── assets/
│ │ │ │ └── custom.glowbom
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── glowbom/
│ │ │ │ └── custom/
│ │ │ │ ├── AiExtensions.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── ui/
│ │ │ │ └── theme/
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ └── res/
│ │ │ ├── drawable/
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── drawable-v24/
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── mipmap-anydpi-v26/
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── values/
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── themes.xml
│ │ │ └── xml/
│ │ │ ├── backup_rules.xml
│ │ │ └── data_extraction_rules.xml
│ │ ├── build.gradle.kts
│ │ ├── gradle/
│ │ │ └── wrapper/
│ │ │ └── gradle-wrapper.properties
│ │ ├── gradle.properties
│ │ ├── gradlew
│ │ ├── gradlew.bat
│ │ └── settings.gradle.kts
│ ├── apple/
│ │ ├── Custom/
│ │ │ ├── AiExtensions.swift
│ │ │ ├── Assets.xcassets/
│ │ │ │ ├── AccentColor.colorset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ ├── ContentView.swift
│ │ │ ├── Custom.entitlements
│ │ │ ├── CustomApp.swift
│ │ │ ├── Preview Content/
│ │ │ │ └── Preview Assets.xcassets/
│ │ │ │ └── Contents.json
│ │ │ └── custom.glowbom
│ │ ├── Custom.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace/
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ ├── xcshareddata/
│ │ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ │ └── xcuserdata/
│ │ │ │ └── jacobilin.xcuserdatad/
│ │ │ │ └── UserInterfaceState.xcuserstate
│ │ │ └── xcuserdata/
│ │ │ └── jacobilin.xcuserdatad/
│ │ │ └── xcschemes/
│ │ │ └── xcschememanagement.plist
│ │ └── README.md
│ ├── glowbom.json
│ ├── prototype/
│ │ ├── assets.json
│ │ └── index.html
│ └── web/
│ ├── .gitignore
│ ├── README.md
│ ├── components.json
│ ├── eslint.config.mjs
│ ├── next.config.mjs
│ ├── package.json
│ ├── postcss.config.js
│ ├── public/
│ │ └── custom.glowbom
│ ├── src/
│ │ ├── app/
│ │ │ ├── components/
│ │ │ │ ├── AiExtensions.tsx
│ │ │ │ └── Custom.tsx
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── components/
│ │ │ └── ui/
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── form.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── pagination.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── toast.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── toggle.tsx
│ │ │ ├── tooltip.tsx
│ │ │ └── use-toast.ts
│ │ ├── lib/
│ │ │ └── utils.ts
│ │ └── models/
│ │ └── types.ts
│ ├── tailwind.config.ts
│ └── tsconfig.json
├── scripts/
│ └── install.sh
└── web/
├── LICENSE
├── index.html
├── package.json
├── src/
│ ├── App.tsx
│ ├── hooks/
│ │ └── useRefineRun.ts
│ ├── lib/
│ │ ├── api.ts
│ │ ├── console-render.ts
│ │ ├── model-catalog.ts
│ │ ├── server-auth.ts
│ │ └── sse.ts
│ ├── main.tsx
│ ├── styles.css
│ └── types/
│ └── opencode.ts
├── tsconfig.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .claude/settings.json
================================================
{
"permissions": {
"allow": [
"Bash(go build:*)"
]
}
}
================================================
FILE: .github/workflows/glowby-release.yml
================================================
name: Release glowby
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
goos: [darwin, linux, windows]
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Build binary
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: '0'
run: |
if [ -d oss/cli ]; then
CLI_DIR="oss/cli"
else
CLI_DIR="cli"
fi
TAG="${GITHUB_REF#refs/tags/}"
SHA="$(git rev-parse --short HEAD)"
DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
EXT=""
if [ "${{ matrix.goos }}" = "windows" ]; then EXT=".exe"; fi
cd "${CLI_DIR}"
go build \
-ldflags "-s -w -X main.version=${TAG} -X main.commit=${SHA} -X main.date=${DATE}" \
-o "glowby-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}" \
.
- name: Create archive
run: |
if [ -d oss/cli ]; then
CLI_DIR="oss/cli"
else
CLI_DIR="cli"
fi
cd "${CLI_DIR}"
BIN="glowby-${{ matrix.goos }}-${{ matrix.goarch }}"
if [ "${{ matrix.goos }}" = "windows" ]; then
mv "${BIN}.exe" glowby.exe
zip "${BIN}.zip" glowby.exe
else
mv "${BIN}" glowby
tar czf "${BIN}.tar.gz" glowby
fi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: glowby-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
oss/cli/glowby-*.tar.gz
oss/cli/glowby-*.zip
cli/glowby-*.tar.gz
cli/glowby-*.zip
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Create release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: artifacts/*
================================================
FILE: .gitignore
================================================
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map
chat/.DS_Store
.DS_Store
/presets/compose/.idea
presets/swiftui/Custom.xcodeproj/xcuserdata/jacobilin.xcuserdatad/xcschemes/xcschememanagement.plist
presets/swiftui/Custom.xcodeproj/project.xcworkspace/xcuserdata/jacobilin.xcuserdatad/UserInterfaceState.xcuserstate
/docs/.react-router/
/docs/.source/
/docs/build/
/docs/node_modules/
.obsidian/workspace.json
.obsidian/core-plugins.json
.obsidian/appearance.json
.obsidian/app.json
project.zip
/web/node_modules
Untitled.md
/web/dist
backend/glowbom-backend
backend/assets/flux-comfyui-workflow.json
backend/assets/128.json
backend/assets/128-api.json
backend/assets/104.json
backend/node/local.ts
backend/package.json
/backend/saved_audio
/backend/saved_images
backend/veo-bridge.js
backend/veo-cli.js
cli/glowby
backend/server
================================================
FILE: AGENTS.md
================================================
# Workspace Instructions
## Commit Messages
- After making code changes, always suggest a one-line conventional commit message.
- Prefer the format `type: summary`.
================================================
FILE: CLI.md
================================================
# glowby CLI
Terminal-first CLI for Glowby OSS. Starts the Go backend and web UI, opens the browser, and manages the full local dev workflow from one command.
## Install
### From GitHub Releases
```sh
curl -fsSL https://raw.githubusercontent.com/glowbom/glowby/main/scripts/install.sh | sh
```
Or set a custom install directory:
```sh
GLOWBY_INSTALL_DIR=~/.local/bin curl -fsSL ... | sh
```
### Build from source
```sh
cd cli
go build -o glowby .
```
## Commands
### `glowby code`
Start the backend, web UI, and open the browser from a local Glowby checkout.
```sh
glowby code # Start Glowby from the current checkout
glowby code /path/to/project # Start Glowby and print a project path hint
```
What it does:
1. Starts the Go backend (`go run .` in `backend/`)
2. Runs `bun install` in `web/` if `node_modules/` is missing
3. Reclaims ports `4569` and `4572` if they are already occupied by a previous Glowby run
4. Starts the web dev server (`bun run dev` in `web/`)
5. Waits for the web server to be ready, then opens the browser
6. If a project path is given, prints the path so you can load it in the UI
Press Ctrl+C to stop both servers.
**Argument parsing:**
- If a positional arg is given and it is an existing directory, it is treated as the project path
- If it is not an existing directory, the command exits with an error
**Finding the Glowby root:** The CLI looks for sibling `backend/` and `web/` directories relative to the binary location or the current working directory and its parent directories.
### `glowby doctor`
Check that required tools are installed.
```sh
glowby doctor
```
Checks for: `go` (required), `bun` (required), `opencode` (required), and a local Glowby checkout with sibling `backend/` and `web/` directories. Returns exit code 1 if required dependencies are missing.
### `glowby version`
```sh
glowby version
```
Prints version, commit hash, and build date. These are injected at build time via ldflags.
## Local Development
```sh
cd cli
# Build
go build -o glowby .
# Build with version info
go build -ldflags "-s -w \
-X main.version=v0.1.0 \
-X main.commit=$(git rev-parse --short HEAD) \
-X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o glowby .
# Run
./glowby version
./glowby doctor
./glowby code
# Vet
go vet ./...
```
## Releases
Releases are built automatically by GitHub Actions when a tag matching `v*` is pushed.
```sh
git tag v0.1.0
git push origin v0.1.0
```
The workflow builds binaries for:
- macOS (amd64, arm64)
- Linux (amd64, arm64)
- Windows (amd64, arm64)
Archives are uploaded to the GitHub Release page.
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Command or dependency failure |
| 2 | Usage error (unknown command, bad args) |
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Glowbom, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Glowby OSS
> We just launched Glowby OSS! [See the announcement](https://x.com/jacobilin/status/2035059308463833292)
**Build anything locally.**
Glowby helps you build production-ready software with coding agents. It is an open source coding agent workflow for real projects. It is built primarily for Glowbom projects, but the workflow can also work with other project structures.
## What It Does
- Make software projects and prototypes production-ready with coding agents
- Run on local projects with ChatGPT login, API keys, or OpenCode config
## Vision
We believe that you should own your code and data. Every line of code Glowby generates lives on your machine, in standard project files you can open with any editor. No vendor lock-in.
## Install
Install the Glowby CLI:
```bash
curl -fsSL https://raw.githubusercontent.com/glowbom/glowby/main/scripts/install.sh | sudo sh
```
For Windows, we recommend using WSL and running the install command inside Ubuntu.
Then clone the repo and enter it:
```bash
git clone https://github.com/glowbom/glowby.git
cd glowby
```
## Quickstart
Glowby needs these tools available on your `PATH`:
- [Go](https://go.dev/)
- [Bun](https://bun.sh/)
- [OpenCode](https://opencode.ai/)
Run the built-in environment check and launch Glowby:
```bash
glowby doctor
glowby code
```
Run those commands from the Glowby repo root, where `backend/` and `web/` live side by side.
## Security Defaults
`glowby code` now hardens the local stack by default:
- Glowby services bind to loopback (`127.0.0.1`) instead of all interfaces
- the backend API requires a per-run bearer token
- the OpenCode bridge runs with `OPENCODE_SERVER_PASSWORD`
To view the generated credentials for the current session, start Glowby with `glowby code --show-local-auth`.
If you launch the stack manually, set equivalent env vars yourself:
```bash
export GLOWBY_BIND_HOST=127.0.0.1
export GLOWBY_SERVER_TOKEN="$(openssl rand -hex 32)"
export OPENCODE_SERVER_PASSWORD="$(openssl rand -hex 32)"
```
Then run the backend with those env vars, and start the web app with:
```bash
export VITE_GLOWBY_SERVER_TOKEN="$GLOWBY_SERVER_TOKEN"
```
## Start Using Glowby OSS
1. Open `http://localhost:4572`
2. Load a local project
3. Choose how you want to run the agent:
- ChatGPT login
- API keys
- OpenCode config
4. Start a refine run
## Cost
You can build with Glowby for free. Run local AI models on your own computer or connect to free cloud models. If you want access to premium models, you can connect a paid account or your own API keys, but you do not need to.
## Requirements And Setup
If `glowby doctor` reports missing tools, install them first and confirm they are available on your `PATH`:
```bash
go version
bun --version
opencode --version
```
If any command is not found, restart your terminal first. If it still does not work, add the tool's install location to your `PATH` or reinstall it using the tool's recommended installer.
On macOS, a common fix is to add the tool's bin directory to your shell profile (usually `~/.zshrc`) and then reload it:
```bash
# Common PATH fixes on macOS
echo 'export PATH="/usr/local/go/bin:$PATH"' >> ~/.zshrc
echo 'export BUN_INSTALL="$HOME/.bun"' >> ~/.zshrc
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> ~/.zshrc
# Add the directory that contains the opencode binary
echo 'export PATH="/path/to/opencode/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
```
If you use Bash instead of zsh, update `~/.bash_profile` or `~/.bashrc` instead.
### Manual fallback
If you prefer to launch the stack without the CLI, run the backend and web app separately:
#### Backend
```bash
cd backend
go run .
```
The backend runs on `http://localhost:4569`.
#### Web app
```bash
cd web
bun install
bun run dev
```
The web app runs on `http://localhost:4572`.
## Using the Bundled Default Project
This repo includes a ready-to-use Glowbom default project in `project/`.
You can use `project/` as your main starting template without logging in to Glowbom.com or downloading a project export first. Just copy the folder, rename it if you want, and start customizing it locally.
The bundled project includes:
- `project/prototype/` - reference design and assets
- `project/apple/` - Apple app project
- `project/android/` - Android app project
- `project/web/` - web app project
- `project/glowbom.json` - project manifest
If you only need some targets, remove the platform folders you do not want:
- Delete `project/apple/` if you do not need Apple platforms
- Delete `project/android/` if you do not need Android
- Delete `project/web/` if you do not need web
- Keep all of them if you want to build every platform in sync from one Glowbom project
## Project Structure
- `backend/` - Go backend
- `project/` - bundled default Glowbom project template
- `web/` - React + Vite web app
- `legacy/` - older Glowby code kept for reference
================================================
FILE: backend/backend_info_page.go
================================================
package main
import (
"encoding/json"
"fmt"
"html"
"net/http"
"os"
"path/filepath"
"strings"
)
const (
glowbyRepositoryURL = "https://github.com/glowbom/glowby"
glowbomWebsiteURL = "https://glowbom.com"
codexAppServerURL = "https://developers.openai.com/codex/app-server/"
glowbomDesktopPDFPath = "docs/2026/Glowbom_Desktop_A_Sketch_to_Software_System.pdf"
)
type backendInfo struct {
Name string `json:"name"`
Summary string `json:"summary"`
HowItWorks string `json:"howItWorks"`
RuntimeURL string `json:"runtimeURL"`
Links []backendInfoLink `json:"links"`
Drivers []backendDriver `json:"drivers"`
ProjectDescriptionURL string `json:"projectDescriptionURL,omitempty"`
ProjectDescriptionPath string `json:"projectDescriptionPath,omitempty"`
}
type backendInfoLink struct {
Label string `json:"label"`
URL string `json:"url"`
}
type backendDriver struct {
ID string `json:"id"`
Label string `json:"label"`
Status string `json:"status"` // available | planned
Description string `json:"description"`
}
func resolveGlowbomDesktopPDFPath() (string, bool) {
candidates := []string{
filepath.FromSlash(filepath.Join("..", glowbomDesktopPDFPath)),
filepath.FromSlash(glowbomDesktopPDFPath),
}
for _, candidate := range candidates {
abs, err := filepath.Abs(candidate)
if err != nil {
continue
}
info, err := os.Stat(abs)
if err != nil || info.IsDir() {
continue
}
return abs, true
}
return "", false
}
func resolveGlowbyPublicAssetPath(fileName string) (string, bool) {
safeName := strings.TrimSpace(fileName)
if safeName == "" || strings.Contains(safeName, "/") || strings.Contains(safeName, "\\") {
return "", false
}
candidates := []string{
filepath.FromSlash(filepath.Join("..", "website", "glowby-oss", "public", safeName)),
filepath.FromSlash(filepath.Join("website", "glowby-oss", "public", safeName)),
}
for _, candidate := range candidates {
abs, err := filepath.Abs(candidate)
if err != nil {
continue
}
info, err := os.Stat(abs)
if err != nil || info.IsDir() {
continue
}
return abs, true
}
return "", false
}
func serveGlowbyPublicAsset(w http.ResponseWriter, r *http.Request, fileName string) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
assetPath, ok := resolveGlowbyPublicAssetPath(fileName)
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Cache-Control", "no-store")
http.ServeFile(w, r, assetPath)
}
func glowbyFaviconHandler(w http.ResponseWriter, r *http.Request) {
serveGlowbyPublicAsset(w, r, "favicon.png")
}
func glowbyLogoSVGHandler(w http.ResponseWriter, r *http.Request) {
serveGlowbyPublicAsset(w, r, "logo-svg.svg")
}
func glowbyBackendInfoPayload() backendInfo {
info := backendInfo{
Name: "Glowby",
Summary: "Choose a project folder and let Glowby finish the engineering work locally.",
HowItWorks: "Glowby manages agent drivers for you, keeps context in one place, and streams every step while code is being improved.",
RuntimeURL: "http://127.0.0.1:" + getAgentPort(),
Links: []backendInfoLink{
{Label: "Glowby OSS repository", URL: glowbyRepositoryURL},
{Label: "Glowbom website", URL: glowbomWebsiteURL},
{Label: "Codex App Server", URL: codexAppServerURL},
{Label: "Backend metadata (JSON)", URL: "/opencode/about"},
{Label: "Backend health", URL: "/opencode/health"},
{Label: "Auth status", URL: "/opencode/auth/status"},
},
Drivers: []backendDriver{
{
ID: "opencode",
Label: "OpenCode",
Status: "available",
Description: "Default driver today. Great for local refine runs and broad model/provider flexibility.",
},
{
ID: "codex-app-server",
Label: "Codex App Server",
Status: "planned",
Description: "Next planned driver. You will be able to switch drivers without changing your project workflow.",
},
},
}
if pdfPath, ok := resolveGlowbomDesktopPDFPath(); ok {
info.ProjectDescriptionURL = "/opencode/about/project-description"
info.ProjectDescriptionPath = pdfPath
info.Links = append(info.Links, backendInfoLink{
Label: "Glowbom Desktop system paper (PDF)",
URL: info.ProjectDescriptionURL,
})
}
return info
}
func normalizedLinkHref(raw string) string {
href := strings.TrimSpace(raw)
if href == "" {
return "#"
}
return href
}
func glowbyBackendHomeHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
info := glowbyBackendInfoPayload()
if strings.Contains(strings.ToLower(r.Header.Get("Accept")), "application/json") {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(info)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
var linksHTML strings.Builder
for _, link := range info.Links {
label := html.EscapeString(strings.TrimSpace(link.Label))
href := html.EscapeString(normalizedLinkHref(link.URL))
if label == "" || href == "" {
continue
}
linksHTML.WriteString(fmt.Sprintf(`
%s`, href, label))
}
var driversHTML strings.Builder
for _, driver := range info.Drivers {
label := html.EscapeString(strings.TrimSpace(driver.Label))
status := "Planned"
if strings.EqualFold(strings.TrimSpace(driver.Status), "available") {
status = "Available"
}
description := html.EscapeString(strings.TrimSpace(driver.Description))
driversHTML.WriteString(fmt.Sprintf(
`%s %s%s
`,
label,
html.EscapeString(status),
description,
))
}
projectPathLine := `Project description PDF: not found locally.
`
if strings.TrimSpace(info.ProjectDescriptionPath) != "" {
projectPathLine = fmt.Sprintf(
`Local PDF path: %s
`,
html.EscapeString(info.ProjectDescriptionPath),
)
}
page := fmt.Sprintf(`
%s
%s
%s
%s
OpenCode runtime: %s
How It Works
- Open the Glowby web UI at
http://127.0.0.1:4572.
- Choose a local Glowbom project folder.
- Pick an agent driver and run
/opencode/refine.
- Watch live logs, answer prompts, and open outputs in your IDE.
`,
html.EscapeString(info.Name),
html.EscapeString(info.Name),
html.EscapeString(info.Summary),
html.EscapeString(info.HowItWorks),
html.EscapeString(info.RuntimeURL),
driversHTML.String(),
linksHTML.String(),
projectPathLine,
)
_, _ = w.Write([]byte(page))
}
func openCodeAboutHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(glowbyBackendInfoPayload())
}
func openCodeProjectDescriptionHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
pdfPath, ok := resolveGlowbomDesktopPDFPath()
if !ok {
http.Error(w, "Project description PDF not found", http.StatusNotFound)
return
}
w.Header().Set("Cache-Control", "no-store")
http.ServeFile(w, r, pdfPath)
}
================================================
FILE: backend/claude_anthropic.go
================================================
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"strings"
"time"
)
const claudeSonnetDefaultModel = "claude-sonnet-4-6"
const claudeOpusDefaultModel = "claude-opus-4-6"
func isClaudeOpusModel(modelID string) bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(modelID)), "claude-opus-")
}
func claudeModelTokenRates(modelID string) (inputCostPer1M float64, outputCostPer1M float64) {
if isClaudeOpusModel(modelID) {
return 5.0, 25.0
}
return 3.0, 15.0
}
// callClaudeDrawToCodeApiFull handles draw-to-code using Claude API with native vision
func callClaudeDrawToCodeApiFull(imageBase64, userPrompt, template, imageSource, apiKey string) (*R1Response, error) {
return callClaudeDrawToCodeApiFullWithModel(imageBase64, userPrompt, template, imageSource, apiKey, claudeSonnetDefaultModel)
}
// callClaudeDrawToCodeApiFullWithModel handles draw-to-code with specific Claude model
func callClaudeDrawToCodeApiFullWithModel(imageBase64, userPrompt, template, imageSource, apiKey, modelID string) (*R1Response, error) {
if apiKey == "" {
return &R1Response{
AIResponse: "No Claude API key provided. Please add your key in Settings.",
TokenUsage: map[string]int{"inputTokens": 0, "outputTokens": 0, "totalTokens": 0},
Cost: 0,
}, nil
}
// 1) Build system prompt
systemPrompt := getSystemPrompt(template, imageSource)
systemPrompt += " Replace @tailwind placeholders with the Tailwind CSS CDN link to load the real framework."
// 2) Build detailed task
detailedTask := buildDetailedTaskDescription(template, imageSource, userPrompt)
// 3) Construct multimodal message with image + text for Claude's native vision
// Claude expects content array with image and text blocks
userContent := []interface{}{
map[string]interface{}{
"type": "image",
"source": map[string]interface{}{
"type": "base64",
"media_type": "image/jpeg",
"data": imageBase64,
},
},
map[string]interface{}{
"type": "text",
"text": strings.Join([]string{
fmt.Sprintf("Detailed Task Description: %s", detailedTask),
fmt.Sprintf("Human: %s", userPrompt),
}, "\n"),
},
}
// 4) Call Claude API with vision support
messages := []map[string]interface{}{
{"role": "user", "content": userContent},
}
// Use max tokens for draw-to-code (Claude Sonnet 4.6 supports up to 64k output tokens)
// Use 32768 tokens (32K) with 4096 thinking budget for comprehensive HTML generation
aiResp, inputTokens, outputTokens, err := callClaudeAPIWithThinkingBudgetAndModel(messages, systemPrompt, apiKey, modelID, 32768, 4096)
if err != nil {
return nil, err
}
// 5) Calculate cost based on model
// Sonnet 4.6: Input $3/1M, Output $15/1M
// Opus 4.6: Input $5/1M, Output $25/1M
inputCostPer1M, outputCostPer1M := claudeModelTokenRates(modelID)
cost := (float64(inputTokens) / 1_000_000.0 * inputCostPer1M) + (float64(outputTokens) / 1_000_000.0 * outputCostPer1M)
return &R1Response{
AIResponse: aiResp,
TokenUsage: map[string]int{
"inputTokens": inputTokens,
"outputTokens": outputTokens,
"totalTokens": inputTokens + outputTokens,
},
Cost: cost,
}, nil
}
// callClaudeDrawToCodeStreaming handles draw-to-code with streaming for specified Claude model
func callClaudeDrawToCodeStreaming(w http.ResponseWriter, imageBase64, userPrompt, template, imageSource, apiKey, modelID string) error {
if apiKey == "" {
return fmt.Errorf("no Claude API key provided")
}
// Build system prompt (reuse existing logic)
systemPrompt := getSystemPrompt(template, imageSource)
systemPrompt += " Replace @tailwind placeholders with the Tailwind CSS CDN link to load the real framework."
// Build detailed task
detailedTask := buildDetailedTaskDescription(template, imageSource, userPrompt)
// Construct multimodal message with image + text for Claude's native vision
userContent := []interface{}{
map[string]interface{}{
"type": "image",
"source": map[string]interface{}{
"type": "base64",
"media_type": "image/jpeg",
"data": imageBase64,
},
},
map[string]interface{}{
"type": "text",
"text": strings.Join([]string{
fmt.Sprintf("Detailed Task Description: %s", detailedTask),
fmt.Sprintf("Human: %s", userPrompt),
}, "\n"),
},
}
messages := []map[string]interface{}{
{"role": "user", "content": userContent},
}
// Call existing streaming function with 32K tokens and 4K thinking budget
return callClaudeAPIStreamingWithModel(w, messages, systemPrompt, apiKey, modelID, 32768)
}
// callClaudeApiGo handles normal chat using Claude API
func callClaudeApiGo(prevMsgs []ChatMessage, newMsg, apiKey string) (*R1Response, error) {
return callClaudeApiGoWithModel(prevMsgs, newMsg, apiKey, claudeSonnetDefaultModel)
}
// callClaudeApiGoWithModel handles normal chat using a specific Claude model
func callClaudeApiGoWithModel(prevMsgs []ChatMessage, newMsg, apiKey, modelID string) (*R1Response, error) {
if apiKey == "" {
return &R1Response{
AIResponse: "No Claude API key provided. Please add your key in Settings.",
TokenUsage: map[string]int{},
Cost: 0,
}, nil
}
// Gather system message
systemMsg := defaultSystemPrompt
var messages []map[string]interface{}
for _, m := range prevMsgs {
if m.Role == "system" {
systemMsg = m.Content
} else if m.Role == "user" || m.Role == "assistant" {
messages = append(messages, map[string]interface{}{
"role": m.Role,
"content": m.Content,
})
}
}
// Add new user message
messages = append(messages, map[string]interface{}{
"role": "user",
"content": newMsg,
})
// Call Claude API with increased output limit for comprehensive responses.
aiResp, inputTokens, outputTokens, err := callClaudeAPIWithThinkingBudgetAndModel(messages, systemMsg, apiKey, modelID, 8192, 2048)
if err != nil {
return nil, err
}
// Calculate cost based on selected model.
inputCostPer1M, outputCostPer1M := claudeModelTokenRates(modelID)
cost := (float64(inputTokens) / 1_000_000.0 * inputCostPer1M) + (float64(outputTokens) / 1_000_000.0 * outputCostPer1M)
return &R1Response{
AIResponse: aiResp,
TokenUsage: map[string]int{
"inputTokens": inputTokens,
"outputTokens": outputTokens,
"totalTokens": inputTokens + outputTokens,
},
Cost: cost,
}, nil
}
// callClaudeAPI makes the actual HTTP request to Anthropic API
func callClaudeAPI(messages []map[string]interface{}, systemPrompt, apiKey string, maxTokens int) (string, int, int, error) {
return callClaudeAPIWithThinkingBudget(messages, systemPrompt, apiKey, maxTokens, 2048)
}
// callClaudeAPIWithThinkingBudget makes the actual HTTP request with custom thinking budget
func callClaudeAPIWithThinkingBudget(messages []map[string]interface{}, systemPrompt, apiKey string, maxTokens int, thinkingBudget int) (string, int, int, error) {
return callClaudeAPIWithThinkingBudgetAndModel(messages, systemPrompt, apiKey, claudeSonnetDefaultModel, maxTokens, thinkingBudget)
}
// callClaudeAPIWithThinkingBudgetAndModel makes the actual HTTP request with custom thinking budget and model
func callClaudeAPIWithThinkingBudgetAndModel(messages []map[string]interface{}, systemPrompt, apiKey, modelID string, maxTokens int, thinkingBudget int) (string, int, int, error) {
// Thinking budget must be less than max_tokens
if maxTokens <= thinkingBudget {
thinkingBudget = maxTokens / 4 // Use quarter for thinking if max_tokens is small
}
reqBody := map[string]interface{}{
"model": modelID,
"max_tokens": maxTokens,
"messages": messages,
"system": systemPrompt,
"thinking": map[string]interface{}{
"type": "enabled",
"budget_tokens": thinkingBudget,
},
}
jsonBytes, _ := json.Marshal(reqBody)
fmt.Printf("[DEBUG] Claude request body: %s\n", string(jsonBytes))
req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewReader(jsonBytes))
if err != nil {
fmt.Printf("[ERROR] Failed to create request: %v\n", err)
return "", 0, 0, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", apiKey)
req.Header.Set("anthropic-version", "2023-06-01")
fmt.Println("[DEBUG] Calling Claude API...")
resp, err := http.DefaultClient.Do(req)
fmt.Printf("[DEBUG] Claude API responded with status: %v, error: %v\n", resp, err)
if err != nil {
return "", 0, 0, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
b, _ := io.ReadAll(resp.Body)
errMsg := fmt.Sprintf("Claude API error (%d): %s", resp.StatusCode, string(b))
fmt.Println("[ERROR]", errMsg)
return "", 0, 0, fmt.Errorf("%s", errMsg)
}
// Parse response
bodyBytes, _ := io.ReadAll(resp.Body)
var claudeResp struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
Thinking string `json:"thinking"` // For extended thinking blocks
} `json:"content"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
}
if err := json.Unmarshal(bodyBytes, &claudeResp); err != nil {
return "", 0, 0, fmt.Errorf("failed to parse Claude response: %v", err)
}
// Extract text from content blocks, including thinking blocks
var responseText strings.Builder
var thinkingText strings.Builder
for _, block := range claudeResp.Content {
if block.Type == "thinking" && block.Thinking != "" {
thinkingText.WriteString(block.Thinking)
} else if block.Text != "" {
responseText.WriteString(block.Text)
}
}
// Prepend thinking in tags if present
var fullResponse strings.Builder
if thinkingText.Len() > 0 {
fullResponse.WriteString("")
fullResponse.WriteString(thinkingText.String())
fullResponse.WriteString("")
}
fullResponse.WriteString(responseText.String())
fmt.Printf("[DEBUG] Claude response: %d input tokens, %d output tokens\n",
claudeResp.Usage.InputTokens, claudeResp.Usage.OutputTokens)
return fullResponse.String(),
claudeResp.Usage.InputTokens,
claudeResp.Usage.OutputTokens,
nil
}
// getMagicEditTool returns the tool definition for magic_edit
func getMagicEditTool() map[string]interface{} {
return map[string]interface{}{
"name": "magic_edit",
"description": "Trigger a visual code edit on the currently loaded project. Use this when the user asks you to make visual or code changes to their project (like changing colors, adding elements, modifying layouts, etc.).",
"input_schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"edit_description": map[string]interface{}{
"type": "string",
"description": "A clear, specific description of what changes to make to the code/design. Be detailed about colors, positions, sizes, etc.",
},
},
"required": []string{"edit_description"},
},
}
}
// callClaudeChatStreaming streams Claude response via SSE to the writer (for chat)
func callClaudeChatStreaming(w http.ResponseWriter, prevMsgs []ChatMessage, newMsg, apiKey string) error {
return callClaudeChatStreamingWithModel(w, prevMsgs, newMsg, apiKey, claudeSonnetDefaultModel)
}
// callClaudeChatStreamingWithModel streams Claude response with specific model
func callClaudeChatStreamingWithModel(w http.ResponseWriter, prevMsgs []ChatMessage, newMsg, apiKey, modelID string) error {
return callClaudeChatStreamingWithModelAndTools(w, prevMsgs, newMsg, apiKey, modelID, nil)
}
// callClaudeChatStreamingWithModelAndTools streams Claude response with specific model and optional tools
func callClaudeChatStreamingWithModelAndTools(w http.ResponseWriter, prevMsgs []ChatMessage, newMsg, apiKey, modelID string, tools []map[string]interface{}) error {
if apiKey == "" {
return fmt.Errorf("no Claude API key provided")
}
// Gather system message
systemMsg := defaultSystemPrompt
var messages []map[string]interface{}
for _, m := range prevMsgs {
if m.Role == "system" {
systemMsg = m.Content
} else if m.Role == "user" || m.Role == "assistant" {
messages = append(messages, map[string]interface{}{
"role": m.Role,
"content": m.Content,
})
}
}
// Add new user message
messages = append(messages, map[string]interface{}{
"role": "user",
"content": newMsg,
})
// Call streaming Claude API with increased output limit
return callClaudeAPIStreamingWithModelAndTools(w, messages, systemMsg, apiKey, modelID, 8192, tools)
}
// callClaudeAPIStreaming streams Claude responses via SSE directly to the HTTP response writer
func callClaudeAPIStreaming(w http.ResponseWriter, messages []map[string]interface{}, systemPrompt, apiKey string, maxTokens int) error {
return callClaudeAPIStreamingWithModel(w, messages, systemPrompt, apiKey, claudeSonnetDefaultModel, maxTokens)
}
// callClaudeAPIStreamingWithModel streams Claude responses with specific model
func callClaudeAPIStreamingWithModel(w http.ResponseWriter, messages []map[string]interface{}, systemPrompt, apiKey, modelID string, maxTokens int) error {
return callClaudeAPIStreamingWithModelAndTools(w, messages, systemPrompt, apiKey, modelID, maxTokens, nil)
}
// callClaudeAPIStreamingWithModelAndTools streams Claude responses with tools support
func callClaudeAPIStreamingWithModelAndTools(w http.ResponseWriter, messages []map[string]interface{}, systemPrompt, apiKey, modelID string, maxTokens int, tools []map[string]interface{}) error {
// Thinking budget must be less than max_tokens
thinkingBudget := 2048
if maxTokens <= thinkingBudget {
thinkingBudget = maxTokens / 2 // Use half for thinking if max_tokens is small
}
reqBody := map[string]interface{}{
"model": modelID,
"max_tokens": maxTokens,
"messages": messages,
"system": systemPrompt,
"stream": true,
"thinking": map[string]interface{}{
"type": "enabled",
"budget_tokens": thinkingBudget,
},
}
// Add tools if provided
if len(tools) > 0 {
reqBody["tools"] = tools
}
jsonBytes, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewReader(jsonBytes))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", apiKey)
req.Header.Set("anthropic-version", "2023-06-01")
fmt.Println("[DEBUG] Calling Claude API with streaming...")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
b, _ := io.ReadAll(resp.Body)
errMsg := fmt.Sprintf("Claude API error (%d): %s", resp.StatusCode, string(b))
fmt.Println("[ERROR]", errMsg)
return fmt.Errorf("%s", errMsg)
}
fmt.Println("[DEBUG] Claude API responded with status 200, starting to read stream...")
// Stream response chunks to client
var responseBuilder strings.Builder
var thinkingBuilder strings.Builder
var thinkingSent bool
var thinkingStart time.Time
var thinkingDurationSent bool
var inputTokens, outputTokens int
var currentThinkingStep strings.Builder
var currentStepTitleSent bool
var currentBlockType string
// Tool use tracking
var currentToolId string
var currentToolName string
var currentToolInput strings.Builder
reader := bufio.NewReader(resp.Body)
for {
line, err := reader.ReadString('\n')
if len(line) > 0 {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "data:") {
dataStr := strings.TrimSpace(line[6:])
var event struct {
Type string `json:"type"`
Index int `json:"index"`
Delta struct {
Type string `json:"type"`
Text string `json:"text"`
Thinking string `json:"thinking"` // For extended thinking deltas
PartialJson string `json:"partial_json"` // For tool use JSON deltas
} `json:"delta"`
ContentBlock struct {
Type string `json:"type"`
Text string `json:"text"`
Thinking string `json:"thinking"`
Id string `json:"id"` // Tool use ID
Name string `json:"name"` // Tool use name
} `json:"content_block"`
Message struct {
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
} `json:"message"`
Usage struct {
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
Error struct {
Type string `json:"type"`
Message string `json:"message"`
} `json:"error"`
}
if e := json.Unmarshal([]byte(dataStr), &event); e == nil {
// Check for error events
if event.Type == "error" || event.Error.Type != "" {
errMsg := fmt.Sprintf("Claude streaming error: %s - %s", event.Error.Type, event.Error.Message)
fmt.Println("[ERROR]", errMsg)
return fmt.Errorf("%s", errMsg)
}
// Debug: log event types
if event.Type != "" {
fmt.Printf("[Claude DEBUG] Event type: %s, block type: %s, index: %d\n", event.Type, event.ContentBlock.Type, event.Index)
}
// Handle content_block_start - track what type of block we're in
if event.Type == "content_block_start" {
currentBlockType = event.ContentBlock.Type
if currentBlockType == "thinking" {
fmt.Printf("[Claude DEBUG] Starting thinking block (index %d)\n", event.Index)
if thinkingStart.IsZero() {
thinkingStart = time.Now()
}
currentThinkingStep.Reset()
currentStepTitleSent = false
} else if currentBlockType == "tool_use" {
// Tool use block starting
currentToolId = event.ContentBlock.Id
currentToolName = event.ContentBlock.Name
currentToolInput.Reset()
fmt.Printf("[Claude DEBUG] Starting tool_use block: %s (id: %s)\n", currentToolName, currentToolId)
}
}
// Handle content_block_stop - send tool_use event when tool block ends
if event.Type == "content_block_stop" && currentBlockType == "tool_use" {
// Parse the accumulated JSON input
inputJSON := currentToolInput.String()
fmt.Printf("[Claude] Tool use complete: %s, input: %s\n", currentToolName, inputJSON)
// Send tool_use event to client
toolUseData := map[string]interface{}{
"tool_use": map[string]interface{}{
"id": currentToolId,
"name": currentToolName,
"input": inputJSON,
},
}
toolUseBytes, _ := json.Marshal(toolUseData)
fmt.Fprintf(w, "data: %s\n\n", toolUseBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
// Reset tool tracking
currentToolId = ""
currentToolName = ""
currentToolInput.Reset()
}
// Handle content_block_delta - process content based on current block type
if event.Type == "content_block_delta" {
// Handle tool_use input JSON deltas
if currentBlockType == "tool_use" && event.Delta.PartialJson != "" {
currentToolInput.WriteString(event.Delta.PartialJson)
} else if currentBlockType == "thinking" && event.Delta.Thinking != "" {
// Thinking content - use Delta.Thinking field, not Delta.Text!
thinkingBuilder.WriteString(event.Delta.Thinking)
currentThinkingStep.WriteString(event.Delta.Thinking)
// Try to extract and send the title once we have a complete title
if !currentStepTitleSent {
text := currentThinkingStep.String()
hasCompleteMarkdown := strings.HasPrefix(text, "**") && strings.Count(text, "**") >= 2
hasNewlineSeparation := strings.Count(text, "\n") >= 2
if hasCompleteMarkdown || hasNewlineSeparation {
stepTitle := extractThinkingStepTitle(text)
if stepTitle != "" {
thinkingStepData := map[string]string{"thinkingStep": stepTitle}
thinkingStepBytes, _ := json.Marshal(thinkingStepData)
fmt.Fprintf(w, "data: %s\n\n", thinkingStepBytes)
fmt.Printf("[Claude] Sending thinking step: %s\n", stepTitle)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
currentStepTitleSent = true
}
}
}
} else if currentBlockType == "text" && event.Delta.Text != "" {
// Text content (actual response)
// Send complete thinking before first content chunk
if !thinkingSent && thinkingBuilder.Len() > 0 {
// Send the last thinking step if we haven't sent its title yet
if !currentStepTitleSent && currentThinkingStep.Len() > 0 {
stepTitle := extractThinkingStepTitle(currentThinkingStep.String())
if stepTitle != "" {
thinkingStepData := map[string]string{"thinkingStep": stepTitle}
thinkingStepBytes, _ := json.Marshal(thinkingStepData)
fmt.Fprintf(w, "data: %s\n\n", thinkingStepBytes)
fmt.Printf("[Claude] Sending final thinking step: %s\n", stepTitle)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
currentStepTitleSent = true
}
}
thinkingText := "" + thinkingBuilder.String() + ""
chunkData := map[string]string{"chunk": thinkingText}
chunkBytes, _ := json.Marshal(chunkData)
fmt.Fprintf(w, "data: %s\n\n", chunkBytes)
fmt.Println("[Claude] Sent complete thinking to client")
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
thinkingSent = true
// Send thinking duration
if !thinkingDurationSent && !thinkingStart.IsZero() {
duration := time.Since(thinkingStart).Seconds()
duration = math.Round(duration*10) / 10
metaData := map[string]any{
"meta": map[string]any{
"thinkingSeconds": duration,
},
}
metaBytes, _ := json.Marshal(metaData)
fmt.Fprintf(w, "data: %s\n\n", metaBytes)
fmt.Printf("[Claude] Sent thinking duration: %.1fs\n", duration)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
thinkingDurationSent = true
}
}
responseBuilder.WriteString(event.Delta.Text)
// Send content chunk to client
chunkData := map[string]string{"chunk": event.Delta.Text}
chunkBytes, _ := json.Marshal(chunkData)
fmt.Fprintf(w, "data: %s\n\n", chunkBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
}
// Extract usage from message_start
if event.Type == "message_start" {
inputTokens = event.Message.Usage.InputTokens
}
// Extract usage from message_delta
if event.Type == "message_delta" {
outputTokens = event.Usage.OutputTokens
// Calculate cost based on model
// Sonnet 4.6: Input $3/1M, Output $15/1M
// Opus 4.6: Input $5/1M, Output $25/1M
inputCostPer1M, outputCostPer1M := claudeModelTokenRates(modelID)
cost := (float64(inputTokens) / 1_000_000.0 * inputCostPer1M) + (float64(outputTokens) / 1_000_000.0 * outputCostPer1M)
// Send done event with usage
doneData := map[string]interface{}{
"done": true,
"tokenUsage": map[string]int{
"inputTokens": inputTokens,
"outputTokens": outputTokens,
"totalTokens": inputTokens + outputTokens,
},
"cost": cost,
}
doneBytes, _ := json.Marshal(doneData)
fmt.Fprintf(w, "data: %s\n\n", doneBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
}
}
}
if err != nil {
if err == io.EOF {
break
}
return err
}
}
fmt.Printf("[Claude] Stream complete: %d input tokens, %d output tokens\n", inputTokens, outputTokens)
return nil
}
================================================
FILE: backend/codex_chatgpt.go
================================================
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"runtime"
"strings"
"time"
)
// buildCodexInput converts ChatMessage history + new user message into the
// structured input array expected by chatgpt.com/backend-api/codex/responses.
// User messages use {"type":"input_text"}, assistant messages use {"type":"output_text"}.
func buildCodexInput(prevMsgs []ChatMessage, newMsg string) []interface{} {
var input []interface{}
for _, m := range prevMsgs {
switch m.Role {
case "user":
input = append(input, map[string]interface{}{
"role": "user",
"content": []map[string]interface{}{
{"type": "input_text", "text": m.Content},
},
})
case "assistant":
input = append(input, map[string]interface{}{
"type": "message",
"role": "assistant",
"content": []map[string]interface{}{
{"type": "output_text", "text": m.Content, "annotations": []string{}},
},
"status": "completed",
})
}
}
input = append(input, map[string]interface{}{
"role": "user",
"content": []map[string]interface{}{
{"type": "input_text", "text": newMsg},
},
})
return input
}
// buildCodexRequestBody builds the standard request body for the Codex Responses API.
func buildCodexRequestBody(instructions string, input []interface{}, reasoningEffort string, stream bool, modelID string) map[string]interface{} {
resolvedModel := normalizeOpenAIModelID(modelID)
return map[string]interface{}{
"model": resolvedModel,
"store": false,
"stream": stream,
"instructions": instructions,
"input": input,
"reasoning": map[string]interface{}{
"effort": reasoningEffort,
"summary": "auto",
},
"text": map[string]interface{}{
"verbosity": "medium",
},
"include": []string{"reasoning.encrypted_content"},
"tool_choice": "auto",
"parallel_tool_calls": true,
}
}
// callChatGPTCodexStreaming proxies a chat request through the ChatGPT backend
// Codex Responses API, using the JWT access_token and chatgpt_account_id.
func callChatGPTCodexStreaming(w http.ResponseWriter, prevMsgs []ChatMessage, newMsg, accessToken, accountID, modelID string) error {
if accessToken == "" {
return fmt.Errorf("no ChatGPT access token provided")
}
if accountID == "" {
return fmt.Errorf("no ChatGPT account ID provided")
}
systemMsg := defaultSystemPrompt
for _, m := range prevMsgs {
if m.Role == "system" {
systemMsg = m.Content
break
}
}
input := buildCodexInput(prevMsgs, newMsg)
reqBody := buildCodexRequestBody(systemMsg, input, "medium", true, modelID)
return doChatGPTCodexRequest(w, reqBody, accessToken, accountID)
}
// callChatGPTCodexDrawToCodeStreaming handles draw-to-code via the ChatGPT backend
// Codex Responses API.
func callChatGPTCodexDrawToCodeStreaming(w http.ResponseWriter, imageBase64, userPrompt, template, imageSource, accessToken, accountID, modelID string) error {
if accessToken == "" {
return fmt.Errorf("no ChatGPT access token provided")
}
if accountID == "" {
return fmt.Errorf("no ChatGPT account ID provided")
}
systemPrompt := getSystemPrompt(template, imageSource)
systemPrompt += " Replace @tailwind placeholders with the Tailwind CSS CDN link to load the real framework."
detailedTask := buildDetailedTaskDescription(template, imageSource, userPrompt)
userMessage := detailedTask + "\n\n[An image/screenshot was provided for reference. Generate the code based on the description above.]"
input := []interface{}{
map[string]interface{}{
"role": "user",
"content": []map[string]interface{}{
{"type": "input_text", "text": userMessage},
},
},
}
reqBody := buildCodexRequestBody(systemPrompt, input, "low", true, modelID)
return doChatGPTCodexRequest(w, reqBody, accessToken, accountID)
}
// callChatGPTCodexNonStreaming makes a non-streaming call through the ChatGPT backend API.
func callChatGPTCodexNonStreaming(prevMsgs []ChatMessage, newMsg, accessToken, accountID, modelID string) (*R1Response, error) {
if accessToken == "" {
return &R1Response{
AIResponse: "No ChatGPT access token provided. Please re-login in AI Settings.",
TokenUsage: map[string]int{},
Cost: 0,
}, nil
}
systemMsg := defaultSystemPrompt
for _, m := range prevMsgs {
if m.Role == "system" {
systemMsg = m.Content
break
}
}
input := buildCodexInput(prevMsgs, newMsg)
reqBody := buildCodexRequestBody(systemMsg, input, "medium", false, modelID)
jsonBytes, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST",
"https://chatgpt.com/backend-api/codex/responses",
bytes.NewReader(jsonBytes))
if err != nil {
return nil, err
}
setChatGPTCodexHeaders(req, accessToken, accountID)
fmt.Println("[CODEX] Calling ChatGPT Codex API (non-streaming)...")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("ChatGPT Codex API error (%d): %s", resp.StatusCode, string(b))
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Parse Responses API format
var result struct {
Output []struct {
Content []struct {
Text string `json:"text"`
} `json:"content"`
} `json:"output"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
}
if err := json.Unmarshal(b, &result); err != nil {
return nil, fmt.Errorf("failed to parse Codex response: %v", err)
}
var text strings.Builder
for _, out := range result.Output {
for _, c := range out.Content {
text.WriteString(c.Text)
}
}
cost := estimateOpenAITextCost(modelID, result.Usage.InputTokens, result.Usage.OutputTokens)
return &R1Response{
AIResponse: text.String(),
TokenUsage: map[string]int{
"inputTokens": result.Usage.InputTokens,
"outputTokens": result.Usage.OutputTokens,
"totalTokens": result.Usage.InputTokens + result.Usage.OutputTokens,
},
Cost: cost,
}, nil
}
// doChatGPTCodexRequest sends a streaming request to the ChatGPT backend Codex API
// and proxies SSE events back to the client in the same format as gpt5_responses_api.go.
func doChatGPTCodexRequest(w http.ResponseWriter, reqBody map[string]interface{}, accessToken, accountID string) error {
jsonBytes, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST",
"https://chatgpt.com/backend-api/codex/responses",
bytes.NewReader(jsonBytes))
if err != nil {
return err
}
setChatGPTCodexHeaders(req, accessToken, accountID)
fmt.Println("[CODEX] Calling ChatGPT Codex API with streaming...")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
b, _ := io.ReadAll(resp.Body)
bodyText := string(b)
return fmt.Errorf("ChatGPT Codex API error (%d): %s", resp.StatusCode, bodyText)
}
// Parse and proxy SSE — same event format as Responses API
var responseBuilder strings.Builder
var reasoningBuilder strings.Builder
var reasoningSent bool
var reasoningStart time.Time
var reasoningDurationSent bool
var inputTokens, outputTokens, reasoningTokens int
var lastSummaryIndex int = -1
var currentThinkingStep strings.Builder
var currentStepTitleSent bool
reader := bufio.NewReader(resp.Body)
for {
line, err := reader.ReadString('\n')
if len(line) > 0 {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "data:") {
dataStr := strings.TrimSpace(line[5:])
if dataStr == "[DONE]" {
break
}
var chunk struct {
Type string `json:"type"`
SequenceNumber int `json:"sequence_number"`
SummaryIndex int `json:"summary_index"`
Delta string `json:"delta"`
Text string `json:"text"`
Response struct {
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
OutputTokensDetails struct {
ReasoningTokens int `json:"reasoning_tokens"`
} `json:"output_tokens_details"`
} `json:"usage"`
} `json:"response"`
}
if e := json.Unmarshal([]byte(dataStr), &chunk); e == nil {
// Handle reasoning delta events
if chunk.Type == "response.reasoning_summary_text.delta" {
if reasoningStart.IsZero() {
reasoningStart = time.Now()
}
if chunk.Delta != "" {
if chunk.SummaryIndex != lastSummaryIndex {
if lastSummaryIndex != -1 {
reasoningBuilder.WriteString("\n\n")
}
currentThinkingStep.Reset()
currentStepTitleSent = false
lastSummaryIndex = chunk.SummaryIndex
}
reasoningBuilder.WriteString(chunk.Delta)
currentThinkingStep.WriteString(chunk.Delta)
if !currentStepTitleSent {
text := currentThinkingStep.String()
hasCompleteMarkdown := strings.HasPrefix(text, "**") && strings.Count(text, "**") >= 2
hasNewlineSeparation := strings.Count(text, "\n") >= 2
if hasCompleteMarkdown || hasNewlineSeparation {
stepTitle := extractThinkingStepTitle(text)
if stepTitle != "" {
thinkingStepData := map[string]string{"thinkingStep": stepTitle}
thinkingStepBytes, _ := json.Marshal(thinkingStepData)
fmt.Fprintf(w, "data: %s\n\n", thinkingStepBytes)
fmt.Printf("[CODEX] Sending thinking step: %s\n", stepTitle)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
currentStepTitleSent = true
}
}
}
}
}
// Handle output text delta events
if chunk.Type == "response.output_text.delta" {
if chunk.Delta != "" {
// Send complete reasoning before first content chunk
if !reasoningSent && reasoningBuilder.Len() > 0 {
if !currentStepTitleSent && currentThinkingStep.Len() > 0 {
stepTitle := extractThinkingStepTitle(currentThinkingStep.String())
if stepTitle != "" {
thinkingStepData := map[string]string{"thinkingStep": stepTitle}
thinkingStepBytes, _ := json.Marshal(thinkingStepData)
fmt.Fprintf(w, "data: %s\n\n", thinkingStepBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
}
thinkingText := "" + reasoningBuilder.String() + ""
chunkData := map[string]string{"chunk": thinkingText}
chunkBytes, _ := json.Marshal(chunkData)
fmt.Fprintf(w, "data: %s\n\n", chunkBytes)
fmt.Println("[CODEX] Sent complete reasoning chain to client")
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
reasoningSent = true
duration := time.Since(reasoningStart).Seconds()
duration = math.Round(duration*10) / 10
metaData := map[string]any{
"meta": map[string]any{
"thinkingSeconds": duration,
},
}
metaBytes, _ := json.Marshal(metaData)
fmt.Fprintf(w, "data: %s\n\n", metaBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
reasoningDurationSent = true
}
responseBuilder.WriteString(chunk.Delta)
chunkData := map[string]string{"chunk": chunk.Delta}
chunkBytes, _ := json.Marshal(chunkData)
fmt.Fprintf(w, "data: %s\n\n", chunkBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
}
// Extract usage from final completed event
if chunk.Type == "response.completed" {
inputTokens = chunk.Response.Usage.InputTokens
outputTokens = chunk.Response.Usage.OutputTokens
reasoningTokens = chunk.Response.Usage.OutputTokensDetails.ReasoningTokens
if !reasoningDurationSent && !reasoningStart.IsZero() {
duration := time.Since(reasoningStart).Seconds()
duration = math.Round(duration*10) / 10
metaData := map[string]any{
"meta": map[string]any{
"thinkingSeconds": duration,
},
}
metaBytes, _ := json.Marshal(metaData)
fmt.Fprintf(w, "data: %s\n\n", metaBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
reasoningDurationSent = true
}
modelID, _ := reqBody["model"].(string)
cost := estimateOpenAITextCost(modelID, inputTokens, outputTokens)
doneData := map[string]interface{}{
"done": true,
"tokenUsage": map[string]int{
"inputTokens": inputTokens,
"outputTokens": outputTokens,
"reasoningTokens": reasoningTokens,
"totalTokens": inputTokens + outputTokens,
},
"cost": cost,
}
doneBytes, _ := json.Marshal(doneData)
fmt.Fprintf(w, "data: %s\n\n", doneBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
}
}
}
if err != nil {
if err == io.EOF {
break
}
return err
}
}
fmt.Printf("[CODEX] Stream complete: %d input tokens, %d output tokens (%d reasoning)\n", inputTokens, outputTokens, reasoningTokens)
return nil
}
// setChatGPTCodexHeaders sets the required headers for ChatGPT backend API calls.
func setChatGPTCodexHeaders(req *http.Request, accessToken, accountID string) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("chatgpt-account-id", accountID)
req.Header.Set("accept", "text/event-stream")
req.Header.Set("User-Agent", fmt.Sprintf("Glowbom (%s %s)", runtime.GOOS, runtime.GOARCH))
}
================================================
FILE: backend/elevenlabs_audio.go
================================================
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
neturl "net/url"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
const (
elevenLabsBaseURL = "https://api.elevenlabs.io"
defaultElevenVoiceID = "JBFqnCBsd6RMkjVDRZzb"
defaultElevenVoiceModel = "eleven_multilingual_v2"
defaultElevenOutputFormat = "mp3_44100_128"
)
type ElevenLabsAudioRequest struct {
Prompt string `json:"prompt"`
AudioType string `json:"audioType"` // "voice" | "sound" | "music"
ElevenLabsKey string `json:"elevenLabsKey,omitempty"`
VoiceID string `json:"voiceId,omitempty"`
VoiceModel string `json:"voiceModel,omitempty"`
SoundModel string `json:"soundModel,omitempty"`
MusicModel string `json:"musicModel,omitempty"`
OutputFormat string `json:"outputFormat,omitempty"`
DurationSeconds float64 `json:"durationSeconds,omitempty"`
PromptInfluence *float64 `json:"promptInfluence,omitempty"`
Loop bool `json:"loop,omitempty"`
ForceInstrumental bool `json:"forceInstrumental,omitempty"`
}
type ElevenLabsAudioResponse struct {
Prompt string `json:"prompt"`
AudioType string `json:"audioType"`
Filename string `json:"filename"`
SavedPath string `json:"saved_path"`
Audio string `json:"audio"`
MimeType string `json:"mimeType"`
}
type ElevenLabsVoicesRequest struct {
ElevenLabsKey string `json:"elevenLabsKey"`
}
type ElevenLabsVoiceOption struct {
VoiceID string `json:"voiceId"`
Name string `json:"name"`
Category string `json:"category,omitempty"`
Description string `json:"description,omitempty"`
PreviewURL string `json:"previewUrl,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}
type ElevenLabsVoicesResponse struct {
Voices []ElevenLabsVoiceOption `json:"voices"`
}
func listElevenLabsVoicesHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req ElevenLabsVoicesRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON body", http.StatusBadRequest)
return
}
apiKey := strings.TrimSpace(req.ElevenLabsKey)
if apiKey == "" {
http.Error(w, "elevenLabsKey is required", http.StatusBadRequest)
return
}
voices, err := fetchElevenLabsVoices(apiKey)
if err != nil {
msg := fmt.Sprintf("[ERROR] ElevenLabs voices fetch failed: %v", err)
fmt.Println(msg)
http.Error(w, msg, http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(ElevenLabsVoicesResponse{Voices: voices})
}
func generateAudioHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req ElevenLabsAudioRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON body", http.StatusBadRequest)
return
}
req.Prompt = strings.TrimSpace(req.Prompt)
audioType := normalizeAudioType(req.AudioType)
apiKey := strings.TrimSpace(req.ElevenLabsKey)
log.Printf("[ELEVENLABS] generation request type=%s prompt=%q", audioType, req.Prompt)
if req.Prompt == "" {
http.Error(w, "prompt is required", http.StatusBadRequest)
return
}
if apiKey == "" {
http.Error(w, "elevenLabsKey is required", http.StatusBadRequest)
return
}
var (
audioBytes []byte
mimeType string
err error
)
switch audioType {
case "voice":
audioBytes, mimeType, err = callElevenLabsVoice(req, apiKey)
case "sound":
audioBytes, mimeType, err = callElevenLabsSound(req, apiKey)
case "music":
audioBytes, mimeType, err = callElevenLabsMusic(req, apiKey)
default:
http.Error(w, "audioType must be one of: voice, sound, music", http.StatusBadRequest)
return
}
if err != nil {
msg := fmt.Sprintf("[ERROR] ElevenLabs %s generation failed: %v", audioType, err)
fmt.Println(msg)
log.Printf("[ELEVENLABS] generation failed type=%s err=%s", audioType, sanitizeElevenLabsError(err))
http.Error(w, msg, http.StatusInternalServerError)
return
}
audioOutputDir := filepath.Join("saved_images", "audio")
if err := os.MkdirAll(audioOutputDir, 0755); err != nil {
fmt.Printf("[WARN] Failed creating %s folder: %v\n", audioOutputDir, err)
}
ext := extensionForAudioMimeType(mimeType)
filename := fmt.Sprintf("%s_%d.%s", audioType, time.Now().UnixNano(), ext)
savedPath := filepath.Join(audioOutputDir, filename)
if err := os.WriteFile(savedPath, audioBytes, 0644); err != nil {
msg := fmt.Sprintf("[ERROR] writing audio file: %v", err)
fmt.Println(msg)
http.Error(w, msg, http.StatusInternalServerError)
return
}
resp := ElevenLabsAudioResponse{
Prompt: req.Prompt,
AudioType: audioType,
Filename: filename,
SavedPath: savedPath,
Audio: fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(audioBytes)),
MimeType: mimeType,
}
log.Printf("[ELEVENLABS] generation success type=%s bytes=%d mime=%s file=%s", audioType, len(audioBytes), mimeType, filename)
json.NewEncoder(w).Encode(resp)
}
func callElevenLabsVoice(req ElevenLabsAudioRequest, apiKey string) ([]byte, string, error) {
voiceID := strings.TrimSpace(req.VoiceID)
if voiceID == "" {
voiceID = defaultElevenVoiceID
}
modelID := normalizeElevenLabsVoiceModel(req.VoiceModel)
if modelID == "" {
modelID = defaultElevenVoiceModel
}
params := neturl.Values{}
outputFormat := strings.TrimSpace(req.OutputFormat)
if outputFormat == "" {
outputFormat = defaultElevenOutputFormat
}
params.Set("output_format", outputFormat)
body := map[string]interface{}{
"text": req.Prompt,
"model_id": modelID,
}
endpoint := fmt.Sprintf("%s/v1/text-to-speech/%s", elevenLabsBaseURL, neturl.PathEscape(voiceID))
audioBytes, mimeType, err := callElevenLabsBinaryAPI(endpoint, params, body, apiKey)
if err != nil && modelID != defaultElevenVoiceModel && isElevenLabsModelNotFound(err) {
log.Printf("[ELEVENLABS][VOICE] model=%s not found, retrying with %s", modelID, defaultElevenVoiceModel)
body["model_id"] = defaultElevenVoiceModel
return callElevenLabsBinaryAPI(endpoint, params, body, apiKey)
}
return audioBytes, mimeType, err
}
func callElevenLabsSound(req ElevenLabsAudioRequest, apiKey string) ([]byte, string, error) {
params := neturl.Values{}
outputFormat := strings.TrimSpace(req.OutputFormat)
if outputFormat == "" {
outputFormat = defaultElevenOutputFormat
}
params.Set("output_format", outputFormat)
body := map[string]interface{}{
"text": req.Prompt,
}
if modelID := strings.TrimSpace(req.SoundModel); modelID != "" {
body["model_id"] = modelID
}
if req.DurationSeconds > 0 {
body["duration_seconds"] = req.DurationSeconds
}
if req.PromptInfluence != nil {
body["prompt_influence"] = *req.PromptInfluence
}
if req.Loop {
body["loop"] = true
}
return callElevenLabsBinaryAPI(elevenLabsBaseURL+"/v1/sound-generation", params, body, apiKey)
}
func callElevenLabsMusic(req ElevenLabsAudioRequest, apiKey string) ([]byte, string, error) {
params := neturl.Values{}
outputFormat := strings.TrimSpace(req.OutputFormat)
if outputFormat == "" {
outputFormat = defaultElevenOutputFormat
}
params.Set("output_format", outputFormat)
body := map[string]interface{}{
"prompt": req.Prompt,
}
if modelID := strings.TrimSpace(req.MusicModel); modelID != "" {
body["model_id"] = modelID
}
if req.DurationSeconds > 0 {
body["music_length_ms"] = int(req.DurationSeconds * 1000.0)
}
if req.ForceInstrumental {
body["is_instrumental"] = true
}
return callElevenLabsBinaryAPI(elevenLabsBaseURL+"/v1/music", params, body, apiKey)
}
func callElevenLabsBinaryAPI(endpoint string, query neturl.Values, body map[string]interface{}, apiKey string) ([]byte, string, error) {
url := endpoint
if len(query) > 0 {
url += "?" + query.Encode()
}
reqBody, err := json.Marshal(body)
if err != nil {
return nil, "", fmt.Errorf("failed to marshal request: %w", err)
}
request, err := http.NewRequest("POST", url, bytes.NewReader(reqBody))
if err != nil {
return nil, "", fmt.Errorf("failed to create request: %w", err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("xi-api-key", apiKey)
modelID, _ := body["model_id"].(string)
promptText := ""
if text, ok := body["text"].(string); ok {
promptText = text
} else if prompt, ok := body["prompt"].(string); ok {
promptText = prompt
}
log.Printf("[ELEVENLABS] request endpoint=%s model=%s prompt=%q", endpoint, strings.TrimSpace(modelID), promptText)
response, err := http.DefaultClient.Do(request)
if err != nil {
return nil, "", fmt.Errorf("failed to call ElevenLabs API: %w", err)
}
defer response.Body.Close()
data, err := io.ReadAll(response.Body)
if err != nil {
return nil, "", fmt.Errorf("failed to read ElevenLabs response: %w", err)
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
log.Printf("[ELEVENLABS] error endpoint=%s status=%d body=%s", endpoint, response.StatusCode, truncateElevenLabsLog(string(data), 320))
return nil, "", fmt.Errorf("ElevenLabs API error (status %d): %s", response.StatusCode, string(data))
}
mimeType := inferAudioMimeType(response.Header.Get("Content-Type"), data)
log.Printf("[ELEVENLABS] success endpoint=%s status=%d bytes=%d mime=%s", endpoint, response.StatusCode, len(data), mimeType)
return data, mimeType, nil
}
func fetchElevenLabsVoices(apiKey string) ([]ElevenLabsVoiceOption, error) {
request, err := http.NewRequest("GET", elevenLabsBaseURL+"/v1/voices", nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
request.Header.Set("xi-api-key", apiKey)
response, err := http.DefaultClient.Do(request)
if err != nil {
return nil, fmt.Errorf("failed to call ElevenLabs API: %w", err)
}
defer response.Body.Close()
data, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed to read ElevenLabs response: %w", err)
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("ElevenLabs API error (status %d): %s", response.StatusCode, string(data))
}
var decoded struct {
Voices []struct {
VoiceID string `json:"voice_id"`
Name string `json:"name"`
Category string `json:"category"`
Description string `json:"description"`
PreviewURL string `json:"preview_url"`
Labels map[string]string `json:"labels"`
} `json:"voices"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
return nil, fmt.Errorf("failed to parse ElevenLabs response: %w", err)
}
voices := make([]ElevenLabsVoiceOption, 0, len(decoded.Voices))
for _, v := range decoded.Voices {
if strings.TrimSpace(v.VoiceID) == "" {
continue
}
voices = append(voices, ElevenLabsVoiceOption{
VoiceID: v.VoiceID,
Name: v.Name,
Category: v.Category,
Description: v.Description,
PreviewURL: v.PreviewURL,
Labels: v.Labels,
})
}
sort.SliceStable(voices, func(i, j int) bool {
left := strings.ToLower(strings.TrimSpace(voices[i].Name))
right := strings.ToLower(strings.TrimSpace(voices[j].Name))
if left == right {
return voices[i].VoiceID < voices[j].VoiceID
}
return left < right
})
return voices, nil
}
func normalizeAudioType(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "voice", "speech", "tts":
return "voice"
case "sound", "sfx", "sound_effect":
return "sound"
case "music", "song":
return "music"
default:
return strings.ToLower(strings.TrimSpace(value))
}
}
func normalizeElevenLabsVoiceModel(value string) string {
normalized := strings.ToLower(strings.TrimSpace(value))
if normalized == "" {
return ""
}
switch normalized {
case "default", "standard", "base", "multilingual", "multilingual_v2":
return defaultElevenVoiceModel
default:
return strings.TrimSpace(value)
}
}
func isElevenLabsModelNotFound(err error) bool {
if err == nil {
return false
}
lower := strings.ToLower(err.Error())
return strings.Contains(lower, "model_not_found") ||
(strings.Contains(lower, "model id") && strings.Contains(lower, "does not exist"))
}
func sanitizeElevenLabsError(err error) string {
if err == nil {
return ""
}
return truncateElevenLabsLog(strings.ReplaceAll(strings.TrimSpace(err.Error()), "\n", " "), 320)
}
func truncateElevenLabsLog(value string, max int) string {
trimmed := strings.TrimSpace(value)
if len(trimmed) <= max {
return trimmed
}
if max < 4 {
return trimmed[:max]
}
return trimmed[:max-3] + "..."
}
func inferAudioMimeType(contentType string, data []byte) string {
trimmed := strings.TrimSpace(strings.ToLower(contentType))
if trimmed != "" {
if semi := strings.Index(trimmed, ";"); semi >= 0 {
trimmed = strings.TrimSpace(trimmed[:semi])
}
if trimmed != "" && trimmed != "application/octet-stream" {
return trimmed
}
}
if len(data) >= 3 {
if string(data[:3]) == "ID3" {
return "audio/mpeg"
}
// MPEG frame sync.
if data[0] == 0xFF && (data[1]&0xE0) == 0xE0 {
return "audio/mpeg"
}
}
if len(data) >= 12 {
if string(data[:4]) == "RIFF" && string(data[8:12]) == "WAVE" {
return "audio/wav"
}
}
if len(data) >= 4 {
if string(data[:4]) == "OggS" {
return "audio/ogg"
}
if string(data[:4]) == "fLaC" {
return "audio/flac"
}
}
if len(data) >= 12 && string(data[4:8]) == "ftyp" {
return "audio/mp4"
}
return "audio/mpeg"
}
func extensionForAudioMimeType(mimeType string) string {
switch strings.ToLower(strings.TrimSpace(mimeType)) {
case "audio/wav", "audio/wave", "audio/x-wav":
return "wav"
case "audio/ogg":
return "ogg"
case "audio/flac":
return "flac"
case "audio/mp4", "audio/x-m4a":
return "m4a"
default:
return "mp3"
}
}
================================================
FILE: backend/fireworks_fireworks.go
================================================
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
)
const fireworksChatCompletionsURL = "https://api.fireworks.ai/inference/v1/chat/completions"
const (
fireworksChatMaxTokens = 4096
fireworksDrawToCodeMaxTokens = 16384
fireworksDefaultTemperature = 0.6
// Hold back a small tail so normalization can safely fix near-boundary chunks.
fireworksStreamNormalizeHoldback = 256
)
var (
compactCommentCodeRegex = regexp.MustCompile(`(?m)(//[^\n]*?)\s+((?:const|let|var|function|document\.|window\.|if\s*\(|for\s*\(|[A-Za-z_$][A-Za-z0-9_$]*\.|[A-Za-z_$][A-Za-z0-9_$]*\())`)
hexColorPercentCompactRegex = regexp.MustCompile(`(#[0-9A-Fa-f]{3,8})(\d{1,3}%)`)
transitionAllCompactRegex = regexp.MustCompile(`transition:\s*all([0-9]+(?:\.[0-9]+)?s)`)
animationDurationCompactRegex = regexp.MustCompile(`animation:\s*([a-zA-Z_-][a-zA-Z0-9_-]*)([0-9]+(?:\.[0-9]+)?s)`)
boxShadowDoubleZeroCompact = regexp.MustCompile(`box-shadow:\s*00([0-9]+px)`)
boxShadowCompactTwoOffsetRegex = regexp.MustCompile(`box-shadow:\s*0([0-9]+px)([0-9]+px)`)
fireworksGenericDataURIRegex = regexp.MustCompile(`(?is)data:[^"'\s>]+;base64,[A-Za-z0-9+/=\s]{128,}`)
fireworksImgSrcBase64Regex = regexp.MustCompile(`(?is)
]*\bsrc\s*=\s*["'][A-Za-z0-9+/=\s]{256,}["']`)
fireworksImageSigBase64Regex = regexp.MustCompile(`(?is)["'](?:iVBORw0KGgo|/9j/|R0lGOD|UklGR|Qk0)[A-Za-z0-9+/=\s]{256,}["']`)
fireworksFunctionalityConstRE = regexp.MustCompile(`(?i)\bfunctionality\s+const\b`)
fireworksThinkTagRegex = regexp.MustCompile(`(?is)?think>`)
// Go's regexp (RE2) rejects counted repeats above 1000.
fireworksLongBase64RunRegex = regexp.MustCompile(`[A-Za-z0-9+/]{1000,}={0,2}`)
)
func callFireworksDrawToCodeApiFull(imageBase64, userPrompt, template, imageSource, apiKey, fireworksModel string) (*R1Response, error) {
if strings.TrimSpace(apiKey) == "" {
return &R1Response{
AIResponse: "No Fireworks API key provided. Please add your key in Settings.",
TokenUsage: map[string]int{"inputTokens": 0, "outputTokens": 0, "totalTokens": 0},
Cost: 0,
}, nil
}
messages := buildFireworksDrawToCodeMessages(imageBase64, userPrompt, template, imageSource, fireworksModel, apiKey)
aiResp, inputTokens, outputTokens, err := callFireworksChatCompletions(messages, apiKey, fireworksDrawToCodeMaxTokens, fireworksDefaultTemperature, fireworksModel, true)
if err != nil {
return nil, err
}
return &R1Response{
AIResponse: aiResp,
TokenUsage: map[string]int{
"inputTokens": inputTokens,
"outputTokens": outputTokens,
"totalTokens": inputTokens + outputTokens,
},
Cost: 0,
}, nil
}
func callFireworksDrawToCodeStreaming(w http.ResponseWriter, imageBase64, userPrompt, template, imageSource, apiKey, fireworksModel string) error {
if strings.TrimSpace(apiKey) == "" {
return fmt.Errorf("no Fireworks API key provided")
}
messages := buildFireworksDrawToCodeMessages(imageBase64, userPrompt, template, imageSource, fireworksModel, apiKey)
return callFireworksChatCompletionsStreaming(w, messages, apiKey, fireworksDrawToCodeMaxTokens, fireworksDefaultTemperature, fireworksModel, true)
}
func callFireworksApiGo(prevMsgs []ChatMessage, newMsg, apiKey, fireworksModel, attachmentBase64, attachmentMime string) (*R1Response, error) {
if strings.TrimSpace(apiKey) == "" {
return &R1Response{
AIResponse: "No Fireworks API key provided. Please add your key in Settings.",
TokenUsage: map[string]int{"inputTokens": 0, "outputTokens": 0, "totalTokens": 0},
Cost: 0,
}, nil
}
messages := buildFireworksChatMessages(prevMsgs, newMsg, fireworksModel, attachmentBase64, attachmentMime)
aiResp, inputTokens, outputTokens, err := callFireworksChatCompletions(messages, apiKey, fireworksChatMaxTokens, fireworksDefaultTemperature, fireworksModel, false)
if err != nil {
return nil, err
}
return &R1Response{
AIResponse: aiResp,
TokenUsage: map[string]int{
"inputTokens": inputTokens,
"outputTokens": outputTokens,
"totalTokens": inputTokens + outputTokens,
},
Cost: 0,
}, nil
}
func callFireworksChatStreaming(w http.ResponseWriter, prevMsgs []ChatMessage, newMsg, apiKey, fireworksModel, attachmentBase64, attachmentMime string) error {
if strings.TrimSpace(apiKey) == "" {
return fmt.Errorf("no Fireworks API key provided")
}
messages := buildFireworksChatMessages(prevMsgs, newMsg, fireworksModel, attachmentBase64, attachmentMime)
return callFireworksChatCompletionsStreaming(w, messages, apiKey, fireworksChatMaxTokens, fireworksDefaultTemperature, fireworksModel, false)
}
func buildFireworksChatMessages(prevMsgs []ChatMessage, newMsg, fireworksModel, attachmentBase64, attachmentMime string) []map[string]interface{} {
systemMsg := defaultSystemPrompt
for _, m := range prevMsgs {
if m.Role == "system" {
systemMsg = m.Content
}
}
messages := []map[string]interface{}{
{
"role": "system",
"content": systemMsg,
},
}
for _, m := range prevMsgs {
if m.Role == "user" || m.Role == "assistant" {
messages = append(messages, map[string]interface{}{
"role": m.Role,
"content": m.Content,
})
}
}
userContent := interface{}(newMsg)
trimmedAttachment := strings.TrimSpace(attachmentBase64)
trimmedMime := strings.TrimSpace(attachmentMime)
if trimmedAttachment != "" && trimmedMime != "" {
resolvedModel := normalizeFireworksModelID(fireworksModel)
if fireworksModelSupportsImageInput(resolvedModel) {
if strings.HasPrefix(strings.ToLower(trimmedMime), "image/") {
userContent = []interface{}{
map[string]interface{}{
"type": "text",
"text": newMsg,
},
map[string]interface{}{
"type": "image_url",
"image_url": map[string]interface{}{
"url": fireworksAttachmentDataURI(trimmedAttachment, trimmedMime),
},
},
}
fmt.Printf("[Fireworks DEBUG] inline image attachment enabled model=%s mime=%s\n", resolvedModel, trimmedMime)
} else {
fmt.Printf("[Fireworks DEBUG] attachment ignored for model=%s (non-image mime=%q)\n", resolvedModel, trimmedMime)
}
} else {
fmt.Printf("[Fireworks DEBUG] attachment ignored for model=%s (image input unsupported)\n", resolvedModel)
}
}
messages = append(messages, map[string]interface{}{
"role": "user",
"content": userContent,
})
return messages
}
func callFireworksChatCompletions(messages []map[string]interface{}, apiKey string, maxTokens int, temperature float64, fireworksModel string, enforceNoEmbeddedImageData bool) (string, int, int, error) {
resolvedModel := normalizeFireworksModelID(fireworksModel)
hasImage := fireworksMessagesContainImage(messages)
logFireworksRequestSummary(resolvedModel, false, hasImage, maxTokens, messages)
reqBody := map[string]interface{}{
"model": resolvedModel,
"max_tokens": maxTokens,
"top_p": 1,
"top_k": 40,
"presence_penalty": 0,
"frequency_penalty": 0,
"temperature": temperature,
"messages": messages,
}
if prettyBody, err := json.MarshalIndent(reqBody, "", " "); err == nil {
fmt.Printf("[Fireworks DEBUG] request body stream=false >>>\n%s\n<<< END Fireworks request body\n", string(prettyBody))
}
jsonBytes, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", fireworksChatCompletionsURL, bytes.NewReader(jsonBytes))
if err != nil {
return "", 0, 0, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", 0, 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
bodyText := strings.TrimSpace(string(body))
fmt.Printf("[Fireworks ERROR] model=%s stream=false status=%d hasImage=%v body=%s\n", resolvedModel, resp.StatusCode, hasImage, truncateForLog(bodyText))
return "", 0, 0, fmt.Errorf("Fireworks API error (%d): %s", resp.StatusCode, bodyText)
}
var completion struct {
Choices []struct {
Message struct {
Content interface{} `json:"content"`
ReasoningContent interface{} `json:"reasoning_content"`
} `json:"message"`
ReasoningContent interface{} `json:"reasoning_content"`
FinishReason interface{} `json:"finish_reason"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
if err := json.NewDecoder(resp.Body).Decode(&completion); err != nil {
return "", 0, 0, fmt.Errorf("failed to parse Fireworks response: %w", err)
}
if len(completion.Choices) == 0 {
return "", 0, 0, fmt.Errorf("no choices in Fireworks response")
}
content := fireworkContentToText(completion.Choices[0].Message.Content)
reasoning := fireworkContentToText(completion.Choices[0].Message.ReasoningContent)
if strings.TrimSpace(reasoning) == "" {
reasoning = fireworkContentToText(completion.Choices[0].ReasoningContent)
}
if !enforceNoEmbeddedImageData && strings.TrimSpace(reasoning) != "" {
content = "" + strings.TrimSpace(reasoning) + "" + content
}
if enforceNoEmbeddedImageData {
content = normalizeFireworksDrawToCodeOutput(content)
}
if enforceNoEmbeddedImageData {
if reason, blocked := detectFireworksEmbeddedImagePayload(content); blocked {
return "", 0, 0, embeddedImagePayloadError(reason)
}
}
finishReason := normalizeFinishReason(completion.Choices[0].FinishReason)
if finishReason != "" {
fmt.Printf("[Fireworks] completion finish_reason=%s model=%s stream=false\n", finishReason, resolvedModel)
}
if strings.EqualFold(finishReason, "length") {
fmt.Printf("[Fireworks WARN] completion reached max_tokens=%d model=%s stream=false\n", maxTokens, resolvedModel)
if enforceNoEmbeddedImageData {
return "", 0, 0, fmt.Errorf("Fireworks draw-to-code output was truncated at max_tokens=%d. Please regenerate with a shorter prompt.", maxTokens)
}
}
inputTokens := completion.Usage.PromptTokens
outputTokens := completion.Usage.CompletionTokens
if inputTokens == 0 && outputTokens == 0 {
inputTokens = estimateTokenCountForMessages(messages)
outputTokens = estimateTokenCount(content)
}
return content, inputTokens, outputTokens, nil
}
func callFireworksChatCompletionsStreaming(w http.ResponseWriter, messages []map[string]interface{}, apiKey string, maxTokens int, temperature float64, fireworksModel string, enforceNoEmbeddedImageData bool) error {
resolvedModel := normalizeFireworksModelID(fireworksModel)
hasImage := fireworksMessagesContainImage(messages)
logFireworksRequestSummary(resolvedModel, true, hasImage, maxTokens, messages)
reqBody := map[string]interface{}{
"model": resolvedModel,
"max_tokens": maxTokens,
"top_p": 1,
"top_k": 40,
"presence_penalty": 0,
"frequency_penalty": 0,
"temperature": temperature,
"messages": messages,
"stream": true,
}
if prettyBody, err := json.MarshalIndent(reqBody, "", " "); err == nil {
fmt.Printf("[Fireworks DEBUG] request body stream=true >>>\n%s\n<<< END Fireworks request body\n", string(prettyBody))
}
jsonBytes, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", fireworksChatCompletionsURL, bytes.NewReader(jsonBytes))
if err != nil {
return err
}
req.Header.Set("Accept", "text/event-stream, application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
bodyText := strings.TrimSpace(string(body))
fmt.Printf("[Fireworks ERROR] model=%s stream=true status=%d hasImage=%v body=%s\n", resolvedModel, resp.StatusCode, hasImage, truncateForLog(bodyText))
return fmt.Errorf("Fireworks API error (%d): %s", resp.StatusCode, bodyText)
}
contentType := strings.ToLower(strings.TrimSpace(resp.Header.Get("Content-Type")))
fmt.Printf("[Fireworks] stream response model=%s hasImage=%v contentType=%q\n", resolvedModel, hasImage, contentType)
// Some models/multimodal paths may ignore stream=true and return a regular JSON completion.
// Handle that gracefully instead of silently returning an empty streamed result.
if !strings.Contains(contentType, "text/event-stream") {
body, _ := io.ReadAll(resp.Body)
bodyText := strings.TrimSpace(string(body))
fmt.Printf("[Fireworks] stream fallback model=%s hasImage=%v contentType=%q body=%s\n", resolvedModel, hasImage, contentType, truncateForLog(bodyText))
var completion struct {
Choices []struct {
Message struct {
Content interface{} `json:"content"`
ReasoningContent interface{} `json:"reasoning_content"`
} `json:"message"`
ReasoningContent interface{} `json:"reasoning_content"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
} `json:"usage"`
}
if err := json.Unmarshal(body, &completion); err != nil {
return fmt.Errorf("Fireworks stream fallback parse error: %w", err)
}
if len(completion.Choices) == 0 {
return fmt.Errorf("Fireworks stream fallback returned no choices")
}
content := fireworkContentToText(completion.Choices[0].Message.Content)
reasoning := fireworkContentToText(completion.Choices[0].Message.ReasoningContent)
if strings.TrimSpace(reasoning) == "" {
reasoning = fireworkContentToText(completion.Choices[0].ReasoningContent)
}
if !enforceNoEmbeddedImageData && strings.TrimSpace(reasoning) != "" {
content = "" + strings.TrimSpace(reasoning) + "" + content
}
if enforceNoEmbeddedImageData {
content = normalizeFireworksDrawToCodeOutput(content)
}
if enforceNoEmbeddedImageData {
if reason, blocked := detectFireworksEmbeddedImagePayload(content); blocked {
return embeddedImagePayloadError(reason)
}
}
if strings.TrimSpace(content) == "" {
return fmt.Errorf("Fireworks stream fallback returned empty content")
}
if err := emitFireworksStreamChunk(w, content); err != nil {
return err
}
promptTokens := completion.Usage.PromptTokens
completionTokens := completion.Usage.CompletionTokens
if promptTokens == 0 && completionTokens == 0 {
promptTokens = estimateTokenCountForMessages(messages)
completionTokens = estimateTokenCount(content)
}
return emitFireworksStreamDone(w, promptTokens, completionTokens)
}
reader := bufio.NewReader(resp.Body)
var responseBuilder strings.Builder
// Keep real-time streaming for draw-to-code UX.
bufferDrawOutput := false
promptTokens := 0
completionTokens := 0
dataEvents := 0
parseErrors := 0
finishReasonLength := false
seenFinishReason := ""
rollingTail := ""
normalizedEmitted := 0
var reasoningBuilder strings.Builder
var reasoningSent bool
var reasoningStartTimeSet bool
var reasoningStartTimestamp int64
var reasoningDurationSent bool
lastThinkingStep := ""
for {
line, err := reader.ReadString('\n')
if len(line) > 0 {
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "data:") {
dataEvents++
dataStr := strings.TrimSpace(trimmedLine[5:])
if dataStr == "" {
continue
}
if dataStr == "[DONE]" {
break
}
var chunk struct {
Choices []struct {
Delta struct {
Content interface{} `json:"content"`
ReasoningContent interface{} `json:"reasoning_content"`
} `json:"delta"`
Message struct {
Content interface{} `json:"content"`
ReasoningContent interface{} `json:"reasoning_content"`
} `json:"message"`
ReasoningContent interface{} `json:"reasoning_content"`
FinishReason interface{} `json:"finish_reason"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
} `json:"usage"`
}
if unmarshalErr := json.Unmarshal([]byte(dataStr), &chunk); unmarshalErr != nil {
parseErrors++
if parseErrors <= 3 {
fmt.Printf("[Fireworks WARN] stream chunk parse failed model=%s hasImage=%v err=%v raw=%s\n", resolvedModel, hasImage, unmarshalErr, truncateForLog(dataStr))
}
continue
}
if chunk.Usage.PromptTokens > 0 {
promptTokens = chunk.Usage.PromptTokens
completionTokens = chunk.Usage.CompletionTokens
}
if len(chunk.Choices) > 0 {
reasoningChunk := fireworkContentToText(chunk.Choices[0].Delta.ReasoningContent)
if strings.TrimSpace(reasoningChunk) == "" {
reasoningChunk = fireworkContentToText(chunk.Choices[0].Message.ReasoningContent)
}
if strings.TrimSpace(reasoningChunk) == "" {
reasoningChunk = fireworkContentToText(chunk.Choices[0].ReasoningContent)
}
if !enforceNoEmbeddedImageData && strings.TrimSpace(reasoningChunk) != "" {
if !reasoningStartTimeSet {
reasoningStartTimeSet = true
reasoningStartTimestamp = time.Now().UnixMilli()
placeholderStep := "Thinking..."
thinkingStepData := map[string]string{"thinkingStep": placeholderStep}
thinkingStepBytes, _ := json.Marshal(thinkingStepData)
fmt.Fprintf(w, "data: %s\n\n", thinkingStepBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
lastThinkingStep = placeholderStep
}
reasoningBuilder.WriteString(reasoningChunk)
// Emit better step titles as soon as enough reasoning text arrives.
stepTitle := extractFireworksThinkingStepTitle(reasoningBuilder.String())
if isMeaningfulThinkingStepTitle(stepTitle) && stepTitle != lastThinkingStep {
thinkingStepData := map[string]string{"thinkingStep": stepTitle}
thinkingStepBytes, _ := json.Marshal(thinkingStepData)
fmt.Fprintf(w, "data: %s\n\n", thinkingStepBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
lastThinkingStep = stepTitle
}
}
reason := normalizeFinishReason(chunk.Choices[0].FinishReason)
if reason != "" {
seenFinishReason = reason
if strings.EqualFold(reason, "length") {
finishReasonLength = true
}
}
content := fireworkContentToText(chunk.Choices[0].Delta.Content)
if strings.TrimSpace(content) == "" {
content = fireworkContentToText(chunk.Choices[0].Message.Content)
}
if strings.TrimSpace(content) == "" {
continue
}
if !enforceNoEmbeddedImageData && !reasoningSent && reasoningBuilder.Len() > 0 {
thinkingText := "" + strings.TrimSpace(reasoningBuilder.String()) + ""
if err := emitFireworksStreamChunk(w, thinkingText); err != nil {
return err
}
reasoningSent = true
}
if enforceNoEmbeddedImageData {
candidate := rollingTail + content
if reason, blocked := detectFireworksEmbeddedImagePayload(candidate); blocked {
return embeddedImagePayloadError(reason)
}
if len(candidate) > 4096 {
rollingTail = candidate[len(candidate)-4096:]
} else {
rollingTail = candidate
}
}
responseBuilder.WriteString(content)
if !bufferDrawOutput {
chunkToEmit := content
if enforceNoEmbeddedImageData {
normalizedCurrent := normalizeFireworksDrawToCodeOutput(responseBuilder.String())
safeLen := len(normalizedCurrent) - fireworksStreamNormalizeHoldback
if safeLen < 0 {
safeLen = 0
}
if safeLen > normalizedEmitted {
chunkToEmit = normalizedCurrent[normalizedEmitted:safeLen]
normalizedEmitted = safeLen
} else {
chunkToEmit = ""
}
}
if chunkToEmit != "" {
if err := emitFireworksStreamChunk(w, chunkToEmit); err != nil {
return err
}
}
}
if !enforceNoEmbeddedImageData && !reasoningDurationSent && reasoningStartTimeSet {
durationSeconds := float64(time.Now().UnixMilli()-reasoningStartTimestamp) / 1000.0
metaData := map[string]interface{}{
"meta": map[string]float64{
"thinkingSeconds": durationSeconds,
},
}
metaBytes, _ := json.Marshal(metaData)
fmt.Fprintf(w, "data: %s\n\n", metaBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
reasoningDurationSent = true
}
}
}
}
if err != nil {
if err == io.EOF {
break
}
return err
}
}
finalText := responseBuilder.String()
if enforceNoEmbeddedImageData {
finalText = normalizeFireworksDrawToCodeOutput(finalText)
}
if !bufferDrawOutput && enforceNoEmbeddedImageData {
if normalizedEmitted < len(finalText) {
if err := emitFireworksStreamChunk(w, finalText[normalizedEmitted:]); err != nil {
return err
}
normalizedEmitted = len(finalText)
}
responseBuilder.Reset()
responseBuilder.WriteString(finalText)
}
if bufferDrawOutput {
if enforceNoEmbeddedImageData {
finalText = normalizeFireworksDrawToCodeOutput(finalText)
}
responseBuilder.Reset()
responseBuilder.WriteString(finalText)
if strings.TrimSpace(finalText) != "" {
if err := emitFireworksStreamChunk(w, finalText); err != nil {
return err
}
}
}
if promptTokens == 0 && completionTokens == 0 {
promptTokens = estimateTokenCountForMessages(messages)
completionTokens = estimateTokenCount(responseBuilder.String())
}
if strings.TrimSpace(responseBuilder.String()) == "" {
return fmt.Errorf("Fireworks stream ended without content (model=%s, hasImage=%v, events=%d)", resolvedModel, hasImage, dataEvents)
}
if !enforceNoEmbeddedImageData && !reasoningDurationSent && reasoningStartTimeSet {
durationSeconds := float64(time.Now().UnixMilli()-reasoningStartTimestamp) / 1000.0
metaData := map[string]interface{}{
"meta": map[string]float64{
"thinkingSeconds": durationSeconds,
},
}
metaBytes, _ := json.Marshal(metaData)
fmt.Fprintf(w, "data: %s\n\n", metaBytes)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
reasoningDurationSent = true
}
if enforceNoEmbeddedImageData {
if reason, blocked := detectFireworksEmbeddedImagePayload(responseBuilder.String()); blocked {
return embeddedImagePayloadError(reason)
}
}
if seenFinishReason != "" {
fmt.Printf("[Fireworks] stream finish_reason=%s model=%s\n", seenFinishReason, resolvedModel)
}
if finishReasonLength {
fmt.Printf("[Fireworks WARN] stream reached max_tokens=%d model=%s\n", maxTokens, resolvedModel)
if enforceNoEmbeddedImageData {
return fmt.Errorf("Fireworks draw-to-code output was truncated at max_tokens=%d. Please regenerate with a shorter prompt.", maxTokens)
}
}
fmt.Printf("[Fireworks] stream complete model=%s hasImage=%v events=%d parseErrors=%d chars=%d\n",
resolvedModel, hasImage, dataEvents, parseErrors, responseBuilder.Len())
return emitFireworksStreamDone(w, promptTokens, completionTokens)
}
func fireworkContentToText(content interface{}) string {
switch v := content.(type) {
case string:
return v
case map[string]interface{}:
if text, ok := v["text"].(string); ok {
return text
}
if nested, ok := v["content"]; ok {
return fireworkContentToText(nested)
}
return ""
case []interface{}:
var builder strings.Builder
for _, part := range v {
partMap, ok := part.(map[string]interface{})
if !ok {
continue
}
partType, _ := partMap["type"].(string)
if (partType == "text" || partType == "output_text") && partMap["text"] != nil {
if text, ok := partMap["text"].(string); ok {
builder.WriteString(text)
continue
}
}
if text, ok := partMap["text"].(string); ok {
builder.WriteString(text)
continue
}
if nested, ok := partMap["content"]; ok {
builder.WriteString(fireworkContentToText(nested))
}
}
return builder.String()
default:
return ""
}
}
func estimateTokenCountForMessages(messages []map[string]interface{}) int {
totalChars := 0
for _, msg := range messages {
if content, ok := msg["content"]; ok {
totalChars += len(fireworkContentToText(content))
}
}
return estimateTokenCount(strings.Repeat("x", totalChars))
}
func estimateTokenCount(text string) int {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
return 0
}
return (len(trimmed) + 3) / 4
}
func fireworksMessagesContainImage(messages []map[string]interface{}) bool {
for _, msg := range messages {
content, ok := msg["content"]
if !ok {
continue
}
parts, ok := content.([]interface{})
if !ok {
continue
}
for _, part := range parts {
partMap, ok := part.(map[string]interface{})
if !ok {
continue
}
if partType, _ := partMap["type"].(string); strings.EqualFold(partType, "image_url") {
return true
}
}
}
return false
}
func fireworksModelSupportsImageInput(modelID string) bool {
resolved := strings.ToLower(strings.TrimSpace(normalizeFireworksModelID(modelID)))
switch resolved {
case "accounts/fireworks/models/kimi-k2p5":
return true
default:
return false
}
}
func fireworksAttachmentDataURI(attachmentBase64, attachmentMime string) string {
trimmed := strings.TrimSpace(attachmentBase64)
if strings.HasPrefix(strings.ToLower(trimmed), "data:") {
return trimmed
}
mime := strings.TrimSpace(attachmentMime)
if mime == "" {
mime = "image/jpeg"
}
return fmt.Sprintf("data:%s;base64,%s", mime, trimmed)
}
func buildFireworksDrawToCodeMessages(imageBase64, userPrompt, template, imageSource, fireworksModel, apiKey string) []map[string]interface{} {
effectiveImageSource := imageSource
if !strings.EqualFold(strings.TrimSpace(effectiveImageSource), "Glowby Images") {
effectiveImageSource = "Glowby Images"
fmt.Printf("[Fireworks DEBUG] forcing imageSource to Glowby Images for safe placeholder output (was=%q)\n", imageSource)
}
resolvedModel := normalizeFireworksModelID(fireworksModel)
systemPrompt := getSystemPrompt(template, effectiveImageSource)
systemPrompt += " Replace @tailwind placeholders with the Tailwind CSS CDN link to load the real framework."
systemPrompt += " Glowby Images mode is strict: NEVER embed image bytes in output. NEVER output data: URIs, base64 blobs, blob: URLs, object URLs, or inline binary arrays."
systemPrompt += " Use glowbyimage: placeholders ONLY. Every
must start with src='about:blank', then JavaScript assigns .src from a glowbyimage variable."
systemPrompt += " If you are uncertain about an image, create another descriptive glowbyimage: placeholder instead of embedding bytes."
systemPrompt += " Return a COMPLETE, valid HTML document from through