Contributing
Source development requires the bun CLI on your PATH. The published npm package bundles its own
Bun runtime for users, but this checkout’s scripts run through your local Bun installation.
git clone https://github.com/lidge-jun/opencodex.gitcd opencodexbun installbun run dev:proxy # proxy API in dev modebun run dev:gui # dashboard dev server (another terminal)bun run typecheck # bun x tsc --noEmitbun run test:changed # routine import-graph test selectionbun test tests/routing/router.test.ts # routine focused testbun run test # complete suite (PR-ready / explicit ask)bun run dev remains an alias for bun run dev:proxy. The dashboard dev server is bun run dev:gui;
the packaged dashboard at GET / is produced by bun run build:gui (gui/dist).
Build and test commands
Section titled “Build and test commands”The root package is Bun-native TypeScript; there is no separate server compile step. Use the checked-in scripts so local commands match CI:
bun run typecheck # strict TypeScript checkbun run test:changed # import-graph tests against the resolved dev merge basebun run test # complete tests/ suite (PR-ready / explicit ask)bun test tests/routing/router.test.ts # focused test filebun run build:gui # Vite GUI build + package preparationbun run privacy:scan # credential/privacy scan used by CIbun run prepare:package # refresh package launchers/assetstest:changed selects the first comparison ref that exists, in order: upstream/dev,
origin/dev, then local dev. It reports that ref and the exact git merge-base HEAD <ref>
commit, then passes the merge-base SHA to Bun.
Tests are Bun tests in domain directories that mirror src/: tests/server/, tests/providers/,
tests/adapters/openai/, tests/cli/ and so on. scripts/test-layout/layout.json is the map
and tests/test-layout.test.ts enforces it, so a new test goes into its domain directory and gets
an entry in the map (the tooling test tells you which one is missing). tests/helpers/ holds
shared fixtures and tests/helpers/repo-root.ts is how a test reaches repository files;
tests/e2e-style/ holds broader native-parity scenarios. Keep a focused regression near the
existing tests for the subsystem you change (bun test tests/<domain> runs one subsystem); run
the full suite for shared routing, adapters, config, or server behavior.
The docs site you’re reading lives in docs-site/ (Astro + Starlight):
cd docs-site && bun install && bun devDocs publishing
Section titled “Docs publishing”The public docs publish to GitHub Pages at https://opencodex.me/. The
.github/workflows/deploy-docs.yml workflow runs on main pushes that touch docs-site/** or the
workflow itself, builds docs-site, and deploys the generated site. Before pushing docs changes,
run:
cd docs-sitebun install --frozen-lockfilebun run buildCI and releases
Section titled “CI and releases”GitHub Actions intentionally stay small:
- Cross-platform CI (
.github/workflows/ci.yml) runs on pull requests andmainpushes that touch runtime, tests, package, script, TypeScript, or workflow files. Its Bun matrix covers Linux, Windows, and macOS with install, typecheck, tests, privacy scan, a release-helper build smoke, GUI build, andocx help. A second three-OS lane proves npm global install works without a separately installed Bun by using the package’s bundled runtime. - Release (
.github/workflows/release.yml) is manual. It does not act as a second full CI pipeline; before dry-run or publish it requires the exact release commit (GITHUB_SHA) to already have a successful Cross-platform CI run. - Stale needs-info (
.github/workflows/stale-needs-info.yml) runs daily on the default branch. Open issues labeledneeds-infowith no activity for 14 days get a warning; after 7 more idle days they close as not planned. Any update clears the stale warning. To keep long-lived work open, removeneeds-info(for example when promoting an issue toroadmap). - Issue quality (
.github/workflows/enforce-issue-quality.yml) validates template structure on new and edited issues, applies kind labels (bug,enhancement,provider-compatibility,documentation), and adds orthogonal area labels from the form Area field plus light title/Summary heuristics:provider,account-pool,catalog,gui,cli,proxy,platform,streaming,tools,install, andservice. Kind/process labels stay separate so you can filterbug+account-poolwithout collapsing those axes. Prefer the Area dropdown over inventing per-provider labels. Area: Documentation does not add a second area tag (the docs form already seedsdocumentation). Maintainers can re-apply area labels to all open issues with workflow_dispatchbackfill_open_areasafter the workflow is on the default branch.
Use the helper for releases:
Before running the helper, choose the intended release version and dispatch
.github/workflows/dev-version-bump.yml from the default branch with
intended-version=<version> and mode=pre-move. Review and merge the PR it opens
into dev, then promote to main or preview and run the helper. If dev already
outranks the intended version, the workflow reports changed=false and no bump PR
is needed. Publishing still requires successful CI on the exact release commit.
bun run release <version> # commits/pushes the bump; publish workflow is dry-run by defaultbun run release --bump minor # derive the next patch, minor, or major version from tags and npm channelsbun run release <version> --publish # publish after the CI-gated dry run is understoodbun run release:watch # watch the newest Release workflow run--bump patch|minor|major is an alternative to an explicit version. Once a preview tag opens a
higher version core, --bump patch refuses to continue the older stable patch line; ship that fix
in the open preview core instead.
Branches
Section titled “Branches”dev— the only integration target. Open your pull request here.main— releases only. It moves by maintainer-controlled promotion fromdev; do not open feature pull requests against it.preview— the prerelease train.
The dev2-go line that carried the Go native port has been retired, and the
dual-track carry policy with it. Its history is published read-only at
lidge-jun/opencodex-go-archive.
Bun-native TypeScript on dev is the single runtime line.
Rebase pull requests are welcome. Bringing a stale branch onto the current head is normal contribution rather than noise — note the source commits in the description.
Pull requests
Section titled “Pull requests”- Target
dev. Do not open feature or fix pull requests againstmain. - Branch from the current
devtip, not frommain. The requiredenforce-targetcheck rejects heads whose merge base sits on themaintip while the branch is far behind the pull request base (the failure mode seen in #644). - Write a real description: a Summary of what changed and why, plus a Test plan (or equivalent substance). Empty bodies, placeholder-only text, and descriptions that use escaped
\ninstead of real line breaks fail the check. - If the title or description mentions
gui, include a screenshot of the UI change in the description; theenforce-targetcheck re-runs on description edits until the screenshot is present. - Workflow changes in this repository use
pull_request_target. Updated enforcement logic applies only after the workflow is promoted to the repository default branch — the same operational caveat documented in #631.
Project maintainers
Section titled “Project maintainers”The current maintainers, their responsibilities, and the review and merge policy are documented in
MAINTAINERS.md. GitHub review
ownership for the repository and security-sensitive paths is declared in .github/CODEOWNERS.
Contributor pull requests normally need a maintainer’s approval. A current maintainer with
GitHub maintain or admin access may explicitly integrate a PR into dev, including their
own, without a second maintainer approval. The decision and exact-head verification must be
recorded; CI, security review and outstanding maintainer objections still apply. This exception
does not change main/preview review rules or allow direct pushes, force-pushes or deletion.
Conventions
Section titled “Conventions”- ES Modules only (
import/export), TypeScript,strictmode. Keepbun x tsc --noEmitclean. - ~500 lines per file max — split by responsibility (the
web-search/andvision/sidecars are good examples of small, focused modules behind a singleindex.ts). - Handle async errors at boundaries — sidecars never throw into the request path; they degrade to a graceful marker.
- Structure SOT — current maintainer invariants live in
structure/. Keep public user workflows indocs-site/and historical investigation notes indocs/. - Preserve exports — other modules may depend on them.
Adding a provider to the catalog
Section titled “Adding a provider to the catalog”All provider pickers and seeds derive from the canonical registry (src/providers/registry.ts):
{ id: "my-provider", label: "My Provider", baseUrl: "https://api.example.com/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://example.com/keys", models: ["model-a", "model-b"], defaultModel: "model-a", noVisionModels: ["model-a"], // text-only models → vision sidecar describes images},src/providers/derive.ts feeds that entry into ocx init, ocx provider, dashboard presets,
API-key login, and OAuth config seeds. enrichProviderFromCatalog() copies model metadata and
capability classifications onto the saved provider config. OAuth protocol implementations still
live in src/oauth/; registry metadata alone is not an OAuth flow.
Evidence required for a canonical preset
Section titled “Evidence required for a canonical preset”A registry entry is a maintained promise: opencodex ships the destination that a user’s API key is sent to. A preset therefore needs primary-source evidence, not a working code path. Pull requests that add or promote a provider must supply all of the following in the description:
- The documented OpenAI-compatible endpoints. Link the vendor’s own API reference for the chat
endpoint and, when the entry sets
liveModels: true, for its authenticated model-discovery endpoint (typicallyGET /v1/models). A passing fixture test is not a substitute: it proves our code shape, not the upstream contract. - Terms of service and the operating legal entity. An empty or placeholder legal page does not establish who runs the endpoint or under what terms user traffic is handled.
- Resale or routing authorization for aggregators. A gateway that sells access to Claude, GPT, Gemini, or other third-party models should show its authorization to route to them. Users read a built-in preset as a maintained route, not as an unverified reseller.
- A named maintenance owner. State who updates the preset when the base URL, authentication, or catalog contract changes, and how a break will be reported.
- A citable verification date. Record the primary source and the date it was checked, the same
way
lastVerifiedworks insrc/providers/free-directory.ts. A date on an unverified row asserts provenance nobody produced.
Contributors adding their own service are welcome, and several current presets arrived that way. Disclose the affiliation in the pull-request description so reviewers can weigh it; affiliation is not a reason for rejection, and it does not lower the evidence bar either.
When the evidence is incomplete, the honest home is a reference row in
src/providers/free-directory.ts rather than the canonical registry. Directory rows carry an
explicit verification grade (official, primary, unverified) and are inert: users can still
reach the service through the custom OpenAI-compatible flow, while opencodex avoids advertising a
preset it cannot stand behind. Promote the row to the registry once the evidence above exists.
Adding an adapter
Section titled “Adding an adapter”Implement ProviderAdapter (see Adapters) in src/adapters/,
register its factory in src/adapters/registry.ts, and bridge its output to internal
AdapterEvents. src/server/adapter-resolve.ts selects the effective protocol before delegating
to the registry. Reuse image.ts for image handling and follow openai-chat.ts for ordinary
streaming/tool calls; use fetchResponse only when the adapter owns transport retries, or runTurn
for a genuinely bidirectional transport such as Cursor. Add focused tests under tests/ and export
the factory from src/index.ts when it belongs to the public package API.
Adding a compatibility claim
Section titled “Adding a compatibility claim”Compatibility claims live under src/compatibility/. A claim is narrower than an adapter: it names
the exact provider, normalized upstream base URL, authentication mode, inbound protocol, upstream
protocol, and model ids whose behavior was proved. Do not copy a claim to every provider using the
same adapter or to another destination using the same wire format.
Use one of the versioned dispositions: passthrough, translated, degraded, or unsupported.
Every non-passthrough claim must state its limitation, and every fixture-backed claim must name the
exact assertion ids that prove it. Add the secret-free request vector under
tests/fixtures/compatibility/ and execute it against the production adapter in a focused test.
Compatibility manifests are passive data: the ordinary router, Responses handler, and server
startup path must not import the manifest catalog or activate Compatibility Lab.
Verify before you claim done
Section titled “Verify before you claim done”Run the narrowest command that proves your change — bun run typecheck for types, a focused
bun test tests/<domain>/<name>.test.ts or runtime probe for behavior, then the broader gates appropriate to
the affected surface. opencodex favors small, verifiable commits over large batches.

