Skip to content

[WRONG BRANCH] feat(providers): generalize accounts UI and per-provider fallback - #842

Closed
OnlineChef wants to merge 43 commits into
lidge-jun:mainfrom
OnlineChefGroep:feat/all-provider-account-management
Closed

[WRONG BRANCH] feat(providers): generalize accounts UI and per-provider fallback#842
OnlineChef wants to merge 43 commits into
lidge-jun:mainfrom
OnlineChefGroep:feat/all-provider-account-management

Conversation

@OnlineChef

@OnlineChef OnlineChef commented Aug 1, 2026

Copy link
Copy Markdown

Summary

  • Remove Codex-only Auth nav; Providers owns account/key pools for all providers.
  • Add configurable per-provider fallback list (reuses combo failover engine).
  • Keeps sofie runtime patches reproducible under the Provider Security Plane (OpenCodex remains the runtime plane; no CPM merge).

Test plan

  • bun test ./gui/tests/sidebar-codex-auth.test.ts ./gui/tests/dashboard-tabs.test.ts
  • Provider settings fallback UI smoke
  • Fallback hop Antigravity → DeepSeek on sofie when pool unusable

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Added configurable provider fallback chains for automatic retry routing.
    • Added OmniRoute support, quota refresh controls, budget alerts, usage analytics, and latency statistics.
    • Added dashboard pages for models, traffic, and system management.
    • Added light, dark, and system themes, settings controls, PostHog analytics, and Dutch localization.
    • Added Codex round-robin account rotation and request pacing.
  • Documentation
    • Added release, versioning, roadmap, and OmniRoute setup documentation.
    • Updated multilingual documentation and repository links.
  • Bug Fixes
    • Improved response ID repair and legacy route redirects.

OnlineChef and others added 30 commits July 30, 2026 16:20
tsc -b failed on dev: Storage.tsx imported React's KeyboardEvent type,
which shadowed the DOM type used by its two window keydown listeners, and
Select in ui.tsx declared id twice. Alias the React import and keep a
single id prop so bun run build:gui (tsc -b && vite build) passes again.

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	gui/src/ui.tsx
…d time

- scripts/test.ts: pgrep -f matches whole command lines, so a shell or CI
  wrapper mentioning `bun test --isolate` was reported as a competing runner.
  Confirm each matched pid's executable is Bun via ps before warning.
- scripts/test.ts: compare elapsed milliseconds against 600_000 so a 600.4s run
  no longer rounds down under the threshold; round only for display.
- gui/tests/apikeys-refresh-preserve.test.tsx: replace the fixed 310ms sleep
  before the armed delete confirm with bounded polling for the enabled button.

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
merge: sync upstream/dev to restore Cross-platform CI
Failure-only restart left the proxy stopped after clean exits; always
matches the intended supervisor behavior and keeps RestartSec=5.

Co-authored-by: Cursor <cursoragent@cursor.com>
Enable sticky session affinity and bounded 429 failover across Antigravity
OAuth accounts while keeping the pool off by default and preserving Anthropic
and Codex pool behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
…et is unusable

rotateGoogleAntigravityAccountOn429 binds the session to the alternate before
the caller resolves its credential. A non-terminal refresh failure does not mark
the account needsReauth, so it stays eligible and the stale binding made every
later request in that session replay the account it could not authenticate with.
Release the binding when the credential lookup throws.

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
fix(service): persist Restart=always in generated systemd unit (PR1)
feat(oauth): opt-in Google Antigravity 429 account pool (PR3)
Classify overflow before rotate/cooldown, fail-fast when Retry-After exceeds
60s, and share one OCX upstream-attempt budget across transport retries and
Antigravity/key pool failovers. Cursor abort/EOF fixtures stay non-rotating.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep last-good catalogs across transient discovery failures, escalate probe
backoff until recovery, and optionally hide dead providers from client
pickers while admin Models retains full rows plus reason.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use VALIDATED mode only for strict Gemini 3 requests so older models retain their existing wire behavior while Claude-on-Antigravity remains unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
Rotate sticky Cursor sessions only on explicit pre-output quota failures while preserving existing pool and transport behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
The runTurn quota-rotation path passed `null` as `retryAfter` to
rotateCursorAccountOnQuota, so an upstream `Retry-After: 120` silently degraded
to the generic 60s default and the exhausted account came back into the pool
while the server was still refusing it.

The header was never reachable at that point: the Cursor Connect transport did
not read response headers at all, and Connect reports quota later as an
end-stream error frame, so only the message text survived to the rotation call.

Thread the value through instead:
- live-transport captures `retry-after` from the HTTP/2 response headers.
- transport-retry attaches it to the failing error, the last point where the
  discarded transport is still reachable, as a non-enumerable property so it
  cannot leak into error serialization.
- the cursor adapter surfaces it on the `error` AdapterEvent.
- core.ts passes it to rotateCursorAccountOnQuota.

Reported by greptile on #24.

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…s Provider labels

Aggregate p95 latency/TTFT, cache-read ratio, and 429/502 ratios into /api/usage.
Probe each Antigravity account with its own token+project behind the existing
quota cache/TTL so GUI renders do not stampede upstream.

Co-authored-by: Cursor <cursoragent@cursor.com>
… remote OmniRoute

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
Additive provider.compat metadata (thinking format, session affinity, strict tools,
max-tokens field) without replacing existing capability lists. Document and enforce
OmniRoute self-host on 127.0.0.1:20128 with allowPrivateNetwork + placeholder bearer;
keep auto non-default and models under omniroute/...

Co-authored-by: Cursor <cursoragent@cursor.com>
…mbo failover

f12f33a's unified classifier and global 3-attempt budget broke two
behaviors that the existing suite already pinned. Both tests pass on dev
and failed on this branch across ubuntu, macOS and Windows.

`vertex retry fetch > raw quota errors keep bounded retry counts without
body peeking`: the new gate called `classifyUpstreamResponse` on every
retryable response, including `returnRawErrors` mode. Classification has
to read the body, so a raw 429 was peeked and, once labelled
quota-exhausted, returned after a single attempt instead of the bounded
three. Raw mode exists to hand the untouched upstream response back to
the caller, which is why the quota peek right below has always been
guarded by `!ctx.returnRawErrors`. Split the gate along that line: the
attempt budget and `Retry-After` are header-only signals and still apply
to raw mode, while the classifier moves behind the same guard as the
peek. Normal-mode behavior is unchanged.

`server combo failover 030 activation matrix > failed passthrough child
callbacks stay buffered and only B finalizes`: combo children inherit the
parent options, so every target shared one budget. A leading target that
answers 503 spends all three attempts on its own transport retries
(`retrying (2/3)`, `(3/3)` in the job log), leaving nothing for the next
target, so the combo returned the first target's 503 instead of failing
over. Give each combo target its own budget. The budget bounds retries
against a single upstream — transport retries plus account-pool rotation,
which is what the shared-budget test in
tests/server-google-antigravity-pool-e2e.test.ts covers — whereas a combo
target is a different provider whose fan-out is already bounded by the
target list and per-target cooldowns.

Verified: both previously failing tests pass, the focused retry, budget
and pool suites pass, root `bun test` is down to 3 failures that are
environment-only in this sandbox (they reproduce on a pristine origin/dev
checkout and appear in no CI log), and `tsc --noEmit`, `privacy:scan`,
actionlint and the gui/ gate are clean.

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
… its siblings

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…PR4)

feat(retry): unified outcome classifier and global 3-attempt budget (PR4)
feat(catalog): discovery backoff and opt-in unavailable-model hide (PR5)
feat(google): honor strict Gemini tools with VALIDATED calling (PR6)
feat(oauth): opt-in Cursor quota account pool (PR7)
…abels (PR8)

feat(usage): quality metrics, Antigravity per-account quotas, Proxy vs Provider labels
feat(providers): add compat matrix and OmniRoute loopback policy
* chore(cursor): add Stitch MCP via local launcher

Expose Google Stitch for GUI work without committing the API key.

* chore(cursor): resolve Stitch MCP launcher from PATH

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>

---------

Co-authored-by: OnlineChef <280567955+OnlineChef@users.noreply.github.com>
Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
… the PR8/PR9 merges

The PR8 (9de1d7f) and PR9 (d6b6e97) merges into dev landed hand-rewritten
copies of several files that silently lost content, leaving dev unable to
typecheck, build the GUI, or install the service.

Restore only the referenced-but-missing pieces:

- src/usage/percentiles.ts, src/usage/pricing.ts, src/telemetry/posthog-server.ts:
  re-add the three modules src/usage/summary.ts and src/usage/budgets.ts import.
  Their absence broke `ocx service install` at runtime, failing all three
  Service lifecycle jobs before any typecheck ran.
- src/types.ts: re-add the OcxCodexRequestPacing interface that
  OcxConfig.codexRequestPacing already referenced.
- src/providers/registry.ts: re-add providerMatchesRegistryTransport and
  normalizedProviderEndpoint (imported by router, oauth, auth-cors, and
  model-discovery), the ProviderModelDiscovery* types, the
  preserveCustomDestination and modelDiscovery entry fields, and the Cursor
  Kimi K3 `max` catalog default.
- tests/provider-registry-parity.test.ts: reconcile the four expectations PR9
  left stale when it added the omniroute entry. These assertions never ran on
  dev because the missing registry export aborted the module import.
- gui/src/i18n/{en,de,ko,zh,ru,ja}.ts: restore the 51 keys that GUI code still
  calls t() with. DICTS is typed Record<Locale, Record<TKey, string>>, so all
  six locales are restored together. The eight deliberately retired
  usage/sub/storage workspace keys stay removed.
- gui/src/styles.css: drop the @import of styles-usage-workspace.css and the
  now-orphaned stylesheet. The retired usage workspace left no consumer of its
  usage-workspace-/usw- classes, which gui/tests/usage-layout.test.ts asserts.
- tests/provider-account-quota.test.ts: restore the @agy.example.test fixture
  domain so the privacy scan stops flagging bare-example addresses.

typecheck, build:gui, privacy:scan, gui lint, and the 399 GUI tests are green.
The 3 remaining `bun run test` failures (ocx service CLI usage/diagnostics)
reproduce identically on untouched dev: the sandbox is a Docker container and
trips the "Docker detected" branch.

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
… handling

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
… the PR8/PR9 merges (#32)

fix(dev): restore modules, registry exports, and i18n keys dropped in the PR8/PR9 merges
OnlineChef and others added 10 commits July 30, 2026 20:15
- Release workflow now allows any main-branch release (stable or alpha/preview)
- Preview releases can use any prerelease suffix (e.g. -alpha.1, -beta.1)
- Removed preview branch requirement (we use main for everything)
- Added ROADMAP.md with short/medium/long term plans
… URLs

- Source code repo constants (star-prompt, update/job, update/notify)
- PR target enforcement workflow (dev→main, docs links)
- Issue quality workflow (docs links)
- Issue templates (security, contributing docs URLs)
- Test scripts (expected URLs, closed_by user)
- All READMEs (en, ko, zh-CN, ru, ja): replace lidge-jun URLs with OnlineChefGroep
- GUI (Dashboard.tsx, Models.tsx): fix docs links
- docs-site: astro.config, robots.txt, Landing.astro, all translated docs
- CONTRIBUTING.md: replace upstream contributing link
- tests/update-job.test.ts: fix expected release URL
Release workflow is main-only with prerelease dist-tag rules; point
enforce-pr-target contributing link at the fork CONTRIBUTING.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nch, roadmap

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…n only

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…se shape

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
chore: OnlineChefGroep fork identity (package URLs, ROADMAP, release model)
…cent, shadcn + skins) (#30)

* redesign(gui): adopt ChefGroep design language (warm palette, blue accent, Archivo/JetBrains Mono, shadcn buttons, ripple, strak skin)

Co-authored-by: OnlineChef <OnlineChef@users.noreply.github.com>

* redesign(gui): blue input focus ring, Dutch-first default, keep faithful dark basalt

Co-authored-by: OnlineChef <OnlineChef@users.noreply.github.com>

* redesign(gui): typography system (caps labels, tabular nums, type ladder, tracking) + motion (page reveal, press-physics, matte modal) + gui/DESIGN.md taste guide

Co-authored-by: OnlineChef <OnlineChef@users.noreply.github.com>

* redesign(gui): bulletproof page-reveal via keyed wrapper; align provider-overview cards to design tokens (caps labels, tabular counts)

Co-authored-by: OnlineChef <OnlineChef@users.noreply.github.com>

* redesign(gui): deeper design-system taste — authentic Stroom ripple-bar, neutral nav-active (accent reserved), caps section headers, range/kbd primitives, taste-rules in DESIGN.md

Co-authored-by: OnlineChef <OnlineChef@users.noreply.github.com>

* chore(gui): drop unused ibm-plex-mono after Archivo/JetBrains adoption

Co-authored-by: navarrocorbi-prog <navarrocorbi-prog@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: OnlineChef <OnlineChef@users.noreply.github.com>
Co-authored-by: navarrocorbi-prog <navarrocorbi-prog@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

⚠️ Wrong target branch

This pull request currently targets main, but pull requests must target one of dev.

@OnlineChef Please retarget this PR to dev. All contributions go to dev; main receives only release promotions. See our Contributing guide for details. Thanks! 🙏

Its title has been prefixed with [WRONG BRANCH].

This pull request is being kept as a draft automatically. Once every issue above is resolved, it will be marked ready for review again.

@github-actions github-actions Bot changed the title feat(providers): generalize accounts UI and per-provider fallback [WRONG BRANCH] feat(providers): generalize accounts UI and per-provider fallback Aug 1, 2026
@github-actions
github-actions Bot marked this pull request as draft August 1, 2026 06:51
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a52fdb02-77ff-4a84-acc0-c826e146bc10

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR establishes the fork’s release and documentation infrastructure, adds provider fallback and quota management, introduces Codex account rotation and pacing, adds telemetry and budget tracking, and expands the GUI with new pages, themes, localization, analytics, and provider settings.

Changes

Fork infrastructure and documentation

Layer / File(s) Summary
Release and repository setup
.github/*, CHANGELOG.md, RELEASE_PROCESS.md, ROADMAP.md, VERSIONING.md
Adds Dependabot, security and workflow lint jobs, main-only release validation, fork release documentation, and repository reference updates.
Documentation site and localized guides
docs-site/**, docs/providers/omniroute.md, readme/*
Updates fork URLs, site metadata, navigation generation, provider documentation, sub-agent limitations, localized documentation, and OmniRoute setup guidance.

Provider and routing behavior

Layer / File(s) Summary
Provider fallback and quota flows
src/providers/*, src/server/management/provider-routes.ts, src/server/responses/core.ts, gui/src/components/provider-workspace/*
Adds validated provider fallback chains, synthetic failover planning, targeted quota refresh, fallback editing, and fallback-aware response logging.
Codex account selection and OAuth
src/codex/*, src/oauth/*, src/types.ts
Adds persisted round-robin selection, per-account request pacing, and optional Cursor account selection during forced login.
Usage and telemetry
src/usage/*, src/telemetry/posthog-server.ts
Adds latency statistics, EUR pricing, persisted token and cost budgets, alert actions, and opt-in batched PostHog telemetry.

GUI application

Layer / File(s) Summary
Application shell and pages
gui/src/pages/*, gui/src/route.ts, gui/src/theme.ts, gui/src/styles/*
Adds settings, models, system, and traffic pages; route canonicalization; theme persistence; and the De Pas visual shell.
Localization and analytics
gui/src/i18n/*, gui/src/posthog.ts, gui/.env.example, gui/vite-env.d.ts
Adds Dutch locale support, expanded translations, sanitized PostHog pageviews, and environment typings.
Account and provider navigation
gui/src/App.tsx, gui/src/app-routing.ts, gui/tests/*
Removes the Codex Auth page, redirects legacy hashes to Providers, and updates sidebar and fallback tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement, documentation, chore

Suggested reviewers: wibias, ingwannu, lidge-jun

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main changes: generalizing the accounts UI and adding per-provider fallback support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/all-provider-account-management
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Collapse Codex-only auth nav into Providers, add configurable
per-provider fallback hops, and keep runtime failover on the
existing combo engine so sofie patches stay reproducible.

Co-authored-by: Cursor <cursoragent@cursor.com>

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
@OnlineChef
OnlineChef force-pushed the feat/all-provider-account-management branch from a20daf9 to c23de7a Compare August 1, 2026 07:04

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a20daf9b4c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server/responses/core.ts Outdated
if (!options.comboAttempt && !isThreadSpawnRequest(req.headers)) {
const plan = providerFallbackPlan(config, { provider: route.providerName, modelId: route.modelId });
if (plan) {
return handleComboResponses(req, body, plan.comboId, plan.config, logCtx, options);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid re-expanding continuation input in fallback hops

When a request has a stored previous_response_id, the outer handler has already replaced body.input with the restored history at line 870, but this passes that expanded body—still carrying previous_response_id—into handleComboResponses. Each child then calls handleResponses again and prepends the same stored items a second time, so even the primary attempt for every fallback-enabled provider receives duplicated conversation history, increasing token use and changing model behavior. Pass the original body into the fallback loop or mark the body as already expanded.

Useful? React with 👍 / 👎.

Comment thread src/providers/fallback.ts
Comment on lines +93 to +97
} else if (!Object.hasOwn(providers, name)) {
issues.push({
path: ["fallback", i, "provider"],
message: `fallback[${i}].provider "${name}" is not configured`,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block deletion of providers used by fallback chains

A fallback reference is now invalid whenever its target is absent, but DELETE /api/providers only checks combo dependencies before deleting and saving a provider. If provider a falls back to b, deleting b therefore succeeds and persists a config that this validation rejects; on the next restart, loadConfig backs up the file and returns the default config. Extend the deletion dependency guard to fallback references or remove those references atomically.

Useful? React with 👍 / 👎.

model: t.model.trim(),
}))
: [];
if (targets.length) next.fallback = targets;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Permit fallback edits on the canonical OpenAI provider

When this field is patched for the built-in openai provider, adding next.fallback makes the subsequent providerManagementConfigError(name, next) fail its exact canonical-seed comparison because the registry seed has no fallback key. The settings UI exposes this control for every provider, so OpenAI—the default provider—always returns a 400 instead of saving its chain. Treat fallback as an allowed overlay when validating the canonical seed.

Useful? React with 👍 / 👎.

Comment thread src/codex/pacer.ts Outdated
Comment on lines +70 to +74
export async function codexPaceBeforeSend(config: OcxConfig, accountId: string | null, poolSize?: number): Promise<void> {
const effective = poolSize !== undefined
? resolveEffectivePacing(config, poolSize)
: (config.codexRequestPacing?.enabled ? { enabled: true, minMs: PACE_DEFAULT_MIN_MS, maxMs: PACE_DEFAULT_MAX_MS, ...config.codexRequestPacing } : null);
if (!effective) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invoke the Codex pacer on outbound requests

For configurations with codexRequestPacing.enabled or multi-account round-robin auto-pacing, this function is never imported or called by any production path in the commit; its only callers are unit tests. Consequently outbound Codex requests are dispatched without any configured delay, so both explicit pacing and the advertised automatic pacing have no runtime effect. Await this function immediately before the selected account's upstream send.

Useful? React with 👍 / 👎.

Comment thread docs-site/astro.config.mjs Outdated
// so Starlight must not emit the former /opencodex project-site prefix.
const SITE_URL = "https://opencodex.me";
// Fork docs — not currently deployed, but config stays ready for GitHub Pages.
const SITE_URL = "https://github.com/OnlineChefGroep/opencodex";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the docs origin on the Pages deployment

The existing deploy-docs.yml still builds and deploys docs-site to GitHub Pages on pushes to main, but this sets Astro's site to a GitHub repository page rather than the deployed documentation origin. Generated canonical, sitemap, Open Graph, and JSON-LD URLs therefore point under github.com/OnlineChefGroep/opencodex; with the default project Pages URL, base: "/" also makes root-relative assets miss the /opencodex/ prefix. Configure the actual Pages/custom-domain origin and matching base path.

AGENTS.md reference: AGENTS.md:L17-L18

Useful? React with 👍 / 👎.

Comment thread src/codex/routing.ts Outdated
Comment on lines +720 to +723
const pick = pool[rrCursor % pool.length]!;
rrCursor = (rrCursor + 1) % Math.max(pool.length, 1);
persistRotationCursor(rrCursor);
return { status: "selected", accountId: pick };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve thread affinity in round-robin mode

When codexRotationMode is round-robin, this branch returns the selected account without calling bindThreadAffinity, even when threadId is present. A subsequent request carrying the same x-codex-parent-thread-id therefore looks like another new conversation and advances to a different account, breaking the documented sticky account behavior and potentially losing account-scoped continuation state. Bind the thread to pick before returning, as the normal selection path does.

Useful? React with 👍 / 👎.

Comment thread gui/package.json Outdated
Comment on lines +17 to +20
"@fontsource-variable/archivo": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@tanstack/react-virtual": "^3.14.5",
"posthog-js": "^1.407.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the GUI lockfile for the added dependencies

These three dependencies were added to gui/package.json, but gui/bun.lock was not changed and contains none of them. A clean bun run build:gui executes cd gui && bun install --frozen-lockfile, so dependency installation fails before the GUI can build in CI or during release packaging. Regenerate and commit the GUI lockfile together with this manifest change.

Useful? React with 👍 / 👎.

Comment thread src/usage/budgets.ts
Comment on lines +241 to +245
export function getBudgetTracker(config?: BudgetConfig): BudgetTracker {
if (!cachedTracker) {
cachedTracker = new BudgetTracker(config ?? {});
}
return cachedTracker;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Feed completed usage into the budget tracker

The new budgets configuration cannot produce any alert because no production code calls getBudgetTracker or recordUsage; this module only exports the singleton and is otherwise unreachable. As a result, configured daily/weekly token limits, cost limits, logs, PostHog events, and webhooks all remain inert. Create the tracker from config.budgets, record terminal request usage, and shut it down with the server lifecycle.

Useful? React with 👍 / 👎.

Comment thread RELEASE_PROCESS.md Outdated
Comment on lines +11 to +16
# 3. Create a release commit + tag
git commit -m "chore(release): v$(node -p 'require("./package.json").version')"
git tag -a "v$(node -p 'require("./package.json").version')" -m "v$(node -p 'require("./package.json").version')"

# 4. Push
git push origin main --follow-tags

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Leave release tag creation to the workflow

Following this quick start always blocks a real automated release: it instructs maintainers to create and push v${version} before dispatch, while the workflow's preflight explicitly rejects any pre-existing tag on non-dry runs even when it points to the dispatched commit. Remove the manual tag creation/push from these steps and let the workflow create the tag after publishing, as the automated-release section already describes.

Useful? React with 👍 / 👎.

Comment thread gui/src/posthog.ts Outdated
Comment on lines +11 to +15
export function initPostHog(): void {
const key = posthogKey();
if (!key || typeof window === "undefined") {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Initialize PostHog when configured

Setting VITE_POSTHOG_KEY as documented in gui/.env.example has no effect because initPostHog is never imported or called by main.tsx or any other production module. The SDK therefore never initializes and no hash pageviews are captured. Invoke this initializer from the GUI entry point when the opt-in key is present.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 61

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
docs-site/src/content/docs/ru/contributing.md (1)

80-80: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the contributor branch policy.

The deleted section described the default dev target, the dev2-go integration scope, and maintainer-controlled promotion to main. MAINTAINERS.md still defines these rules. Restore concise guidance or link to MAINTAINERS.md so contributors use the correct merge path.

Based on MAINTAINERS.md, pull requests target dev by default and promotion to main is maintainer-controlled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/ru/contributing.md` at line 80, Restore the
deleted contributor branch-policy guidance in the Russian contributing
documentation, stating that pull requests target dev by default and promotion to
main is controlled by maintainers; also retain the dev2-go integration scope or
link to MAINTAINERS.md for the complete policy.

Source: Path instructions

docs-site/src/content/docs/zh-cn/guides/providers.md (1)

19-22: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document providers[].fallback in both provider guides.

src/providers/fallback.ts:4-13 and src/providers/fallback.ts:50-64 preserve ordered { provider, model } targets. Add this behavior to docs-site/src/content/docs/guides/providers.md and docs-site/src/content/docs/zh-cn/guides/providers.md. State that only retryable failures hop to the next target. Keep the existing rule that OAuth accounts and API-key pools do not fall through between providers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/zh-cn/guides/providers.md` around lines 19 - 22,
Update both provider guide documents to document the providers[].fallback option
as an ordered list of { provider, model } targets, with retries advancing only
on retryable failures. Preserve and state the existing behavior that OAuth
accounts and API-key pools do not fall through between providers, and add the
same guidance to the Chinese guide in appropriate Chinese wording.

Source: Path instructions

docs-site/src/content/docs/guides/claude-code.md (1)

31-33: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the orphan heading or restore its content.

The deleted Auth section leaves ## System environment integration (macOS) immediately followed by another level-2 heading. This renders an empty section. Remove the heading with the deleted section, or restore the missing macOS guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/guides/claude-code.md` around lines 31 - 33,
Remove the orphaned “System environment integration (macOS)” heading from the
documentation, or restore its missing section content; ensure it is not left
immediately before the “Claude Desktop profile” heading as an empty section.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/dependabot.yml:
- Around line 6-7: Update both Dependabot configuration entries to use the "bun"
package ecosystem instead of "npm", preserving their existing directories; add a
separate Bun entry for "/docs-site" only if its dependencies are in scope.
Ensure the .github configuration change receives explicit security review and
maintainer approval.

In @.github/workflows/ci.yml:
- Around line 156-178: The Bun audit workflow currently exposes full
vulnerability details through the uploaded artifact and failure output. Update
the Generate Bun audit report and Upload Bun audit report steps to retain only a
redacted summary publicly, replace any failure-path cat of bun-audit.json with
safe non-sensitive output, route the full report to protected security tooling,
and remove existing non-expired bun-audit artifacts.
- Around line 160-170: Update the audit step around the JSON and human-readable
bun audit commands to capture the JSON report command’s exit status separately
and fail the workflow when report generation fails, even if the high-severity
audit passes. Preserve the human-readable audit status as the value written to
GITHUB_OUTPUT for the vulnerability gate, while ensuring invalid or missing JSON
evidence cannot produce a successful overall step.
- Around line 145-146: Update both actions/checkout steps in the workflow to set
persist-credentials to false, including the checkout step near the existing
“Checkout” name and the second checkout step, so GitHub credentials are not
retained after checkout.

In @.github/workflows/release.yml:
- Around line 113-136: Unify prerelease detection across the release workflow
and scripts/release-notes.ts so alpha, beta, rc, and preview versions follow the
same preview history and carry-note behavior. Update the classification and
related history/carry logic around NPM_DIST_TAG, RELEASE_VERSION, and
release-notes handling to use one shared predicate, or consistently reject
non-preview prerelease identifiers.

In `@docs-site/astro.config.mjs`:
- Around line 5-48: Set a single deployed GitHub Pages origin consistently:
update SITE_URL, related canonical/Open Graph/JSON-LD URLs, and Astro base in
docs-site/astro.config.mjs to the project Pages URL with /opencodex/ (or use the
configured custom origin with /). Update docs-site/public/robots.txt and
docs-site/src/content/docs/contributing.md at line 48 to the same origin and
deployment state, adding a sitemap directive only if the build generates one.

In `@docs-site/src/content/docs/ja/contributing.md`:
- Line 49: The contributing documentation links to the repository instead of the
published Pages sites. Update docs-site/src/content/docs/ja/contributing.md:49
to use https://opencodex.me/ja/ and
docs-site/src/content/docs/ko/contributing.md:49 to use
https://opencodex.me/ko/.

In `@docs-site/src/content/docs/ko/guides/providers.md`:
- Around line 81-85: Update the Korean OAuth provider catalog to match the
English providers page: add the missing GitHub Copilot entry, include the
corresponding ocx login github-copilot command, and change all applicable
six-provider counts to seven. Preserve the existing Korean formatting and
terminology.

In `@docs-site/src/content/docs/ko/guides/sub-agent-surface.md`:
- Line 9: Update all three issue links in the sub-agent surface guide, replacing
the repository owner OnlineChefGroep with lidge-jun while preserving the issue
number and surrounding Korean documentation.

In `@docs-site/src/content/docs/ru/contributing.md`:
- Around line 48-50: Update the public documentation URL in the Russian
contributing documentation text to use https://opencodex.me/ru/ instead of the
current GitHub repository path, leaving the surrounding deployment workflow
description unchanged.

In `@docs-site/src/content/docs/ru/guides/sub-agent-surface.md`:
- Line 49: Update the issue hyperlink in the Russian guide’s Dashboard →
Sub-agent delegation bullet to point to
https://github.com/lidge-jun/opencodex/issues/92, preserving the surrounding
text and link label.
- Line 9: Обновите описание ограничения encrypted NEW_TASK в английской и
русской страницах: для каждого финального маршрута, кроме канонического ChatGPT
forward-провайдера с openai-responses, authMode "forward" и URL ChatGPT Codex,
рантайм немедленно возвращает 400 unreadable_encrypted_agent_task вместо
доставки пустого тела задачи. Укажите, что надёжная доставка V2 требует
plaintext или нативной модели ChatGPT, и синхронизируйте формулировки о
доступных моделях и reasoning effort с записями каталога, совместимыми с v2.

In `@docs-site/src/content/docs/zh-cn/contributing.md`:
- Line 47: Update the public documentation link in the Chinese contributing
documentation to use the deployed site URL https://opencodex.me/zh-cn/ instead
of the GitHub repository path.

In `@docs-site/src/content/docs/zh-cn/guides/providers.md`:
- Around line 174-182: Update the Ollama Cloud model examples in both English
and Chinese documentation to match the registry: use exact IDs such as
qwen3-coder:480b, qwen3.5:397b, and gemma4:31b or clearly labeled family
patterns, and remove gemini-3-flash-preview. Preserve the existing distinction
between noVisionModels and vision-capable models.

In `@docs-site/src/content/docs/zh-cn/guides/sub-agent-surface.md`:
- Line 9: Update the v2 guidance trigger documentation in both the English and
Chinese guides to state that a non-empty injectionModel only enables built-in
guidance when it resolves in the v2 catalog, unless a roster or fallback is
available. Also document the injectionPrompt exception, which can trigger
guidance independently of model resolution, while noting that arbitrary
non-empty model IDs accepted by the API do not necessarily trigger it.

In `@gui/src/components/provider-workspace/ProviderSettings.tsx`:
- Around line 322-339: Update the fallback-row rendering and selection logic in
ProviderSettings so disabled fallbackPeers cannot be selected and subsequent
rows exclude provider/model pairs already selected in earlier rows. Filter the
available providers and modelIds using the current row’s value plus prior
fallback rows, while preserving the current row’s existing selection so it
remains visible; ensure the resulting UI cannot save disabled targets or
duplicate provider/model combinations.
- Around line 33-45: Update fallback handling in ProviderSettings: add a
dirty-tracking fingerprint that preserves rows with either non-empty provider or
model while keeping normalizeFallback for the save payload and validation;
replace the item.fallback reference dependency in the reset effect with this
value-based key; disable or filter disabled providers from fallback selection
options; and deduplicate provider/model pairs before saving so
providerFallbackIssues cannot reject duplicates.

In `@gui/src/components/provider-workspace/ProviderUsage.tsx`:
- Around line 141-158: Add the four quota refresh translation
keys—prov.quotaRefresh, prov.quotaRefreshing, prov.quotaRefreshFailed, and
prov.quotaRefreshAria—to the English translation definitions in en.ts, then add
matching localized entries to de.ts, ko.ts, and zh.ts following the existing
ja.ts and ru.ts wording pattern so ProviderUsage’s refresh button and failure
message are translated and all locale types remain consistent.

In `@gui/src/i18n/de.ts`:
- Around line 1088-1097: add the four missing German translation keys
prov.quotaRefresh, prov.quotaRefreshing, prov.quotaRefreshFailed, and
prov.quotaRefreshAria to de.ts, matching the wording style and semantics used by
the corresponding entries in ja.ts and ru.ts for ProviderUsage.tsx.

In `@gui/src/i18n/en.ts`:
- Around line 864-873: Synchronize the locale key set by adding all 73 keys
referenced by literal t() calls or defined in other locales to the en export,
including the prov.quotaRefresh* keys used by ProviderUsage and the dash.*,
shell.*, sys.*, vk.*, and mod.* groups. Add matching translations or aligned
fallback values to the explicitly typed ja, ru, and nl dictionaries, then
regenerate or reconcile locale dictionaries so TKey and DICTS remain consistent
without excess-property errors.

In `@gui/src/i18n/ja.ts`:
- Around line 236-251: Update the locale key definitions in en.ts to include
every key added in ja.ts, including dash.*, prov.quotaRefresh*, shell.*, sys.*,
vk.*, and mod.*; preserve the existing English locale structure and values so
the typed Record<TKey, string> accepts ja.ts without excess-property errors.
- Around line 1472-1527: Remove the duplicate claude.tabsLabel, claude.tabCode,
claude.tabDesktop, and claudeDesktop.* block from the end of the locale object.
If any wording changes are required, merge them into the original key
definitions in the Combos workspace section while preserving keys such as
claudeDesktop.status.notActiveProfile.

In `@gui/src/i18n/ko.ts`:
- Around line 1108-1117: Update the Korean translation resource alongside the
existing pws.fallback keys to define prov.quotaRefresh, prov.quotaRefreshing,
prov.quotaRefreshFailed, and prov.quotaRefreshAria, matching the meanings and
interpolation requirements used by ProviderUsage.tsx.

In `@gui/src/i18n/nl.ts`:
- Around line 1-200: Update the canonical translation key definitions in en.ts
to include every key used by the nl.ts overrides, including dash supplier/status
keys, shell.*, sys.*, and vk.* entries. Preserve the existing English fallback
behavior by adding appropriate English strings for each key, then keep nl.ts
overrides unchanged so they type-check against the expanded TKey.

In `@gui/src/i18n/ru.ts`:
- Around line 1474-1529: The appended claude.tabsLabel, claude.tabCode,
claude.tabDesktop, and claudeDesktop.* block duplicates the existing translation
keys and overrides their wording while omitting existing keys. Remove this
duplicate block, or apply any intended wording changes directly to the original
claudeDesktop key block while preserving all existing entries.
- Around line 236-251: Update the locale key definitions in en.ts to include
every key added in ru.ts, including the dash.*, prov.quotaRefresh*, shell.*,
sys.*, vk.*, and mod.* entries; then keep ru.ts aligned with the resulting
TKey-derived Record contract. Use the consolidated definitions near the existing
en.ts key declarations and preserve the Russian translations.

In `@gui/src/i18n/shared.ts`:
- Around line 30-41: Update detectInitial to guard both navigator and
navigator.language before lowercasing, returning Dutch only when navigator or
its language is unavailable. Preserve the existing locale matching, but return
English for unrecognized browser languages; also replace the hand-written
stored-value disjunction with validation against the existing LOCALES definition
so new locales remain supported automatically.

In `@gui/src/i18n/zh.ts`:
- Around line 1108-1117: Add the missing Chinese translations for the
`prov.quotaRefresh`, `prov.quotaRefreshing`, `prov.quotaRefreshFailed`, and
`prov.quotaRefreshAria` keys in the locale object near the existing
provider-related translations, matching the meaning and usage expected by
`ProviderUsage.tsx`.

In `@gui/src/oauth-tos-risk.ts`:
- Around line 11-14: Update ELEVATED_RISK and oauthTosRisk so "cursor" remains
classified as elevated risk, preserving ToS acknowledgement in both OAuth entry
points. Update tests/oauth-tos-warning.test.ts to expect "elevated", and fix
AddProviderModal's flow without changing the existing Providers callback
handling of pending.addAccount. Remove the machine-specific provenance comment
from the source and move it to the changelog or PR description.

In `@gui/src/pages/Instellingen.tsx`:
- Around line 17-24: Update the settings modal component around its overlay to
use the native dialog pattern established by Systeem, including modal
presentation, focus management, and Escape dismissal via the dialog cancel flow
while preserving backdrop close behavior. Add type="button" to the close button
and every theme button in the modal, including the controls around the theme
options, so none can submit a containing form.
- Around line 6-10: Update the THEMES definitions and both theme-label render
locations in Instellingen.tsx to use the existing TKey values theme.light,
theme.dark, and theme.system instead of hardcoded Dutch and English strings.
Pass the selected key through t(...) wherever the labels are rendered,
preserving the current theme options and locale behavior.

In `@gui/src/pages/Modellen.tsx`:
- Around line 36-52: Complete the ARIA tabs implementation in Modellen using the
established pattern from Dashboard: assign stable ids and matching aria-controls
to each tab, implement roving tabIndex with ArrowLeft, ArrowRight, Home, and End
keyboard handling, and wrap the selected content in a role="tabpanel" with a
matching id and aria-labelledby. Preserve the existing tab selection and
rendered Models, Combos, and Subagents content.

In `@gui/src/pages/Providers.tsx`:
- Around line 216-221: In the Providers component, move the peerProviders
projection into a useMemo keyed by config?.providers and define it before the
early return. Pass the memoized result to ProviderSettings so its fallbackPeers
memo remains stable across detail renders while preserving the existing provider
fields.

In `@gui/src/pages/Systeem.tsx`:
- Around line 250-252: The Codex Auth deep link and Systeem section target are
inconsistent. In gui/src/pages/Systeem.tsx lines 250-252, align the section id
and activeTarget with a live system sub-hash or remove both props if Providers
remains the destination, and update the deep-link comment accordingly. In
gui/src/app-routing.ts lines 84-88, choose and implement the matching
destination while preserving the sub-target when routing to Systeem. Update
gui/tests/sidebar-codex-auth.test.ts lines 19 and 23 to assert the selected
destination.
- Around line 160-164: Update stopProxy so stopping is always reset in a finally
block after the stop request completes or fails. In the catch path, surface a
clear error message for genuine request failures while preserving the expected
successful disconnect behavior, and keep the dialog-closing flow intact.
- Around line 107-158: Update the checkUpdate, runUpdate, and runSync handlers
to use the existing readJsonIfOk/readJsonOrThrow helpers from ApiKeys.tsx
instead of calling res.json() before checking the response status. Preserve each
handler’s current success data and localized fallback messages, while ensuring
non-JSON HTTP failures are reported through the shared status-based error
handling.
- Around line 168-173: Update copyEndpoint so it checks that navigator.clipboard
and writeText are available before calling them, and route unavailable-API and
synchronous invocation failures through the existing copied indicator path.
Preserve the current success behavior while ensuring the button does not throw
or remain silently unchanged when clipboard access is unavailable.

In `@gui/src/pages/Verkeer.tsx`:
- Line 178: Update the rail container’s focus handling to use onFocusCapture and
onBlur, pausing only for implicit focus interactions and resuming when focus
moves outside the rail. Preserve an explicit user pause by checking the existing
pause state or its controlling mechanism before clearing paused, and ensure
focus transitions between receipt buttons do not resume the tail prematurely.
- Around line 69-103: Update both polling effects around load and tail to use
AbortController instances and a generation/cancellation guard, aborting
in-flight fetches during cleanup and when apiBase changes. Preserve the existing
last-good and state-update behavior while ignoring abort errors and stale
responses. Add a single-flight guard to tail so each interval skips starting a
new /api/logs request until the previous one settles, preventing overlapping
responses from overwriting newer data.

In `@gui/src/styles/depas.css`:
- Around line 62-63: Update the Stylelint configuration governing
value-keyword-case to ignore font-family values, including font-family
declarations and font shorthand where applicable, so the canonical names in
--font-ui and --font-code remain unchanged while CI validation passes.

In `@gui/src/theme.ts`:
- Around line 1-21: The theme persistence and state logic is duplicated between
App and the theme helpers, allowing Instellingen changes to leave App’s theme
state stale. Refactor App’s theme handling to use readTheme and applyTheme, and
connect Instellingen through lifted App state or a shared onThemeChange
callback; verify whether Instellingen is actually imported and rendered before
wiring this path.

In `@gui/src/use-provider-quotas.ts`:
- Around line 64-106: Update the provider quota route in
fetchProviderQuotaReports to honor the provider query parameter by calling
fetchSingleProviderQuotaReport(config, provider) when present. Return the result
in the GUI response shape as { generatedAt, reports: report ? [report] : [] },
while preserving the existing all-provider behavior when provider is absent.

In `@readme/README.ko.md`:
- Line 420: Replace the empty repository link destination for
OnlineChefGroep/opencodex in readme/README.ko.md lines 420-420,
readme/README.ja.md lines 405-405, and readme/README.zh-CN.md lines 396-396 with
the repository URL, preserving the existing localized text and formatting.
- Line 422: Fix repository-root links in readme/README.ko.md:419 and 422,
readme/README.ja.md:404 and 407, readme/README.ru.md:445 and 447, and
readme/README.zh-CN.md:396 and 398 by changing ./docs-site, ./structure, and
./docs to ../-prefixed paths. In readme/README.ru.md:448-449 and 471, change the
CONTRIBUTING.md and SECURITY.md links to repository-root ../ paths. Also replace
the broken reference/configuration/ and contributing/ links in all affected
localized READMEs with the appropriate published locale routes or valid
repository links; leave the empty Korean, Japanese, and Chinese links unchanged.

In `@RELEASE_PROCESS.md`:
- Around line 6-20: The release documentation currently has operators create and
push the version tag before running the workflow. In RELEASE_PROCESS.md lines
6-20, use npm version --no-git-tag-version for versioning, commit and push only
main, and state that the workflow creates and pushes the tag. In VERSIONING.md
lines 35-41, remove git push origin v<version> and document that the release
workflow owns tag creation and pushing.

In `@src/cli/star-prompt.ts`:
- Line 7: Update the GitHub URL in the GUI link within App.tsx, near the
existing link around the App component, replacing the stale lidge-jun/opencodex
repository with https://github.com/OnlineChefGroep/opencodex while preserving
the surrounding link behavior.

In `@src/codex/pacer.ts`:
- Around line 70-85: Require poolSize in codexPaceBeforeSend and always derive
pacing through resolveEffectivePacing, preserving auto-enable for round-robin
pools with more than one account. Update all callers to pass the actual pool
size, and add coverage verifying codexPaceBeforeSend enables pacing for a
multi-account round-robin pool.

In `@src/codex/routing.ts`:
- Around line 710-725: Update the round-robin branch in
resolveCodexAccountForThreadDetailed to pass its threaded now value as the third
argument to getEligiblePoolAccounts, preserving the injected-time behavior used
by the other account-selection checks.
- Around line 27-74: Update persistRotationCursor and
rotationStatePath/loadPersistedRotationCursor to use static imports for node:fs,
node:path, and getConfigDir instead of require calls. Ensure directory creation
tolerates Windows EEXIST for an existing read-only directory without skipping
writeFileSync, either by handling that error or checking existence before
mkdirSync; preserve the existing best-effort persistence behavior.

In `@src/oauth/index.ts`:
- Around line 122-124: Add a focused regression test under tests/ that invokes
the registered Cursor OAuth provider’s login method with forceLogin: true and
verifies the generated URL includes prompt=select_account. Exercise the
provider-table login forwarding path around OAuthProviderDef, rather than
calling loginCursor directly, while preserving the existing direct-login
coverage.

In `@src/providers/fallback.ts`:
- Around line 134-167: Add a one-line console.warn or debug log in
providerFallbackPlan when usable() filtering leaves fewer than two targets,
immediately before the targets.length < 2 return. Include the route
provider/model and indicate that configured fallback targets are disabled or
unusable; preserve the existing silent return for no configured fallback and
other early-return cases.

In `@src/providers/quota.ts`:
- Around line 664-679: Add a focused Bun regression test for the exported
fetchSingleProviderQuotaReport, covering no-cache behavior without creating
cache, matching-cache behavior that replaces only the queried provider while
preserving cache.ts, and invalidationEpoch changes that skip the merge. Place it
alongside existing quota tests and use controlled provider/cache fixtures to
assert each outcome.
- Around line 664-679: Implement provider-scoped quota requests in the provider
management route by detecting the provider query parameter and calling
fetchSingleProviderQuotaReport instead of fetchProviderQuotaReports. Adapt its
single report result into the hook’s expected reports response shape, preserving
the existing aggregate behavior when no provider is supplied. Update
use-provider-quotas to use the scoped response correctly and add focused tests
covering both request paths and response shapes.

In `@src/server/responses/core.ts`:
- Around line 638-647: Sanitize the fallback assigned to requestedModel in
handleComboResponses: when the request body lacks a string model, use
comboIdLabel(comboId) instead of interpolating the raw comboId. Preserve the
incoming string model unchanged and keep the existing comboIdentity behavior
intact.

In `@src/telemetry/posthog-server.ts`:
- Around line 1-12: Update the module header comment to state that telemetry
batches flush using the global fetch API and are posted to the /batch/ endpoint,
matching the implementation at the telemetry delivery call. Keep the existing
timing and batching details unchanged.
- Around line 121-152: Update flush() to always consume or cancel the fetch
response body regardless of res.ok, ensuring connections are released on both
success and failure paths. Remove the misleading “requeue” wording unless actual
requeue logic is added; preserve the current behavior of dropping the spliced
batch.

In `@src/usage/budgets.ts`:
- Around line 241-246: Update getBudgetTracker to wrap new BudgetTracker(config
?? {}) in a try/catch, matching the defensive construction pattern used by
getServerPosthog. On construction failure, avoid propagating the exception and
return the existing nullable/disabled tracker result expected by callers, while
preserving cachedTracker behavior on successful construction.
- Around line 90-108: Update the load() method to validate that
parsed.todayTokens, parsed.weekTokens, and parsed.todayCostEur are numbers
before merging persisted data with fresh. If any counter has an invalid type,
retain the corresponding fresh/default value while preserving valid persisted
fields and the existing rollover behavior.

In `@src/usage/percentiles.ts`:
- Around line 50-64: Update computeLatencyStats to create one sorted copy of
samples, use a percentileFromSorted helper for p50, p95, and p99, and derive min
and max from the sorted array to avoid repeated sorting and spread-argument
limits. Preserve percentile as the wrapper for unsorted inputs by having it sort
a copy and delegate to percentileFromSorted.

In `@src/usage/pricing.ts`:
- Around line 71-85: Update the prefix fallback in lookupPricing to select the
longest matching provider model key rather than the first Object.keys(PRICING)
match, ensuring specific entries such as gpt-4o-mini take precedence over gpt-4o
while preserving the existing exact-match and provider fallback behavior.

In `@VERSIONING.md`:
- Around line 24-33: Update the version transition section in VERSIONING.md to
use a consistent policy: set Current to the version from package.json (2.7.42),
document 1.0.0-alpha.1 as the first fork release, and explicitly identify 1.0.0
as the subsequent stable milestone. Align the surrounding release-history
wording with this sequence.

---

Outside diff comments:
In `@docs-site/src/content/docs/guides/claude-code.md`:
- Around line 31-33: Remove the orphaned “System environment integration
(macOS)” heading from the documentation, or restore its missing section content;
ensure it is not left immediately before the “Claude Desktop profile” heading as
an empty section.

In `@docs-site/src/content/docs/ru/contributing.md`:
- Line 80: Restore the deleted contributor branch-policy guidance in the Russian
contributing documentation, stating that pull requests target dev by default and
promotion to main is controlled by maintainers; also retain the dev2-go
integration scope or link to MAINTAINERS.md for the complete policy.

In `@docs-site/src/content/docs/zh-cn/guides/providers.md`:
- Around line 19-22: Update both provider guide documents to document the
providers[].fallback option as an ordered list of { provider, model } targets,
with retries advancing only on retryable failures. Preserve and state the
existing behavior that OAuth accounts and API-key pools do not fall through
between providers, and add the same guidance to the Chinese guide in appropriate
Chinese wording.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: beaa9ba0-3efd-4b8a-b724-87cb43751042

📥 Commits

Reviewing files that changed from the base of the PR and between 1adad35 and a20daf9.

⛔ Files ignored due to path filters (1)
  • gui/public/provider-icons/omniroute-color.svg is excluded by !**/*.svg
📒 Files selected for processing (116)
  • .github/dependabot.yml
  • .github/scripts/issue-quality.test.cjs
  • .github/workflows/ci.yml
  • .github/workflows/enforce-issue-quality.yml
  • .github/workflows/release.yml
  • CHANGELOG.md
  • RELEASE_PROCESS.md
  • ROADMAP.md
  • VERSIONING.md
  • docs-site/astro.config.mjs
  • docs-site/public/robots.txt
  • docs-site/src/components/Landing.astro
  • docs-site/src/content/docs/contributing.md
  • docs-site/src/content/docs/getting-started/installation.md
  • docs-site/src/content/docs/guides/claude-code.md
  • docs-site/src/content/docs/guides/providers.md
  • docs-site/src/content/docs/guides/sub-agent-surface.md
  • docs-site/src/content/docs/ja/contributing.md
  • docs-site/src/content/docs/ja/getting-started/installation.md
  • docs-site/src/content/docs/ja/guides/providers.md
  • docs-site/src/content/docs/ja/guides/sub-agent-surface.md
  • docs-site/src/content/docs/ja/reference/cli.md
  • docs-site/src/content/docs/ko/contributing.md
  • docs-site/src/content/docs/ko/getting-started/installation.md
  • docs-site/src/content/docs/ko/guides/providers.md
  • docs-site/src/content/docs/ko/guides/sub-agent-surface.md
  • docs-site/src/content/docs/ko/reference/cli.md
  • docs-site/src/content/docs/reference/cli.md
  • docs-site/src/content/docs/ru/contributing.md
  • docs-site/src/content/docs/ru/getting-started/installation.md
  • docs-site/src/content/docs/ru/guides/providers.md
  • docs-site/src/content/docs/ru/guides/sub-agent-surface.md
  • docs-site/src/content/docs/ru/reference/cli.md
  • docs-site/src/content/docs/zh-cn/contributing.md
  • docs-site/src/content/docs/zh-cn/getting-started/installation.md
  • docs-site/src/content/docs/zh-cn/guides/providers.md
  • docs-site/src/content/docs/zh-cn/guides/sub-agent-surface.md
  • docs-site/src/content/docs/zh-cn/reference/cli.md
  • docs/providers/omniroute.md
  • gui/.env.example
  • gui/package.json
  • gui/src/App.tsx
  • gui/src/app-routing.ts
  • gui/src/components/QuotaBars.tsx
  • gui/src/components/provider-workspace/ProviderDetails.tsx
  • gui/src/components/provider-workspace/ProviderSettings.tsx
  • gui/src/components/provider-workspace/ProviderUsage.tsx
  • gui/src/components/provider-workspace/types.ts
  • gui/src/formatUptime.ts
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/nl.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/shared.ts
  • gui/src/i18n/zh.ts
  • gui/src/icons.tsx
  • gui/src/oauth-tos-risk.ts
  • gui/src/pages/Instellingen.tsx
  • gui/src/pages/Modellen.tsx
  • gui/src/pages/Providers.tsx
  • gui/src/pages/Systeem.tsx
  • gui/src/pages/Verkeer.tsx
  • gui/src/pages/providers-shared.ts
  • gui/src/posthog.ts
  • gui/src/provider-workspace/catalog.ts
  • gui/src/route.ts
  • gui/src/styles/depas.css
  • gui/src/styles/provider-quota.css
  • gui/src/styles/provider-workspace-settings.css
  • gui/src/styles/provider-workspace-shell.css
  • gui/src/theme.ts
  • gui/src/use-provider-quotas.ts
  • gui/src/vite-env.d.ts
  • gui/tests/dashboard-tabs.test.ts
  • gui/tests/provider-settings-fallback.test.tsx
  • gui/tests/sidebar-codex-auth.test.ts
  • readme/README.ja.md
  • readme/README.ko.md
  • readme/README.ru.md
  • readme/README.zh-CN.md
  • src/cli/star-prompt.ts
  • src/codex/pacer.ts
  • src/codex/routing.ts
  • src/config.ts
  • src/oauth/cursor.ts
  • src/oauth/index.ts
  • src/providers/fallback.ts
  • src/providers/quota.ts
  • src/providers/registry.ts
  • src/server/auth-cors.ts
  • src/server/management/provider-routes.ts
  • src/server/responses-item-id-repair.ts
  • src/server/responses/core.ts
  • src/telemetry/posthog-server.ts
  • src/types.ts
  • src/update/job.ts
  • src/update/notify.ts
  • src/usage/budgets.ts
  • src/usage/percentiles.ts
  • src/usage/pricing.ts
  • tests/codex-pacer.test.ts
  • tests/codex-rotation.test.ts
  • tests/cursor-oauth.test.ts
  • tests/oauth-tos-warning.test.ts
  • tests/pacer-default.test.ts
  • tests/percentiles.test.ts
  • tests/posthog-server.test.ts
  • tests/pricing.test.ts
  • tests/provider-fallback.test.ts
  • tests/provider-registry-parity.test.ts
  • tests/responses-item-id-repair.test.ts
  • tests/rotation-persist.test.ts
  • tests/server-combo-failover-e2e.test.ts
  • tests/update-job.test.ts

Comment thread .github/dependabot.yml Outdated
Comment on lines +6 to +7
- package-ecosystem: "npm"
directory: "/"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

fd -HI -t f '^(bun\.lock|bun\.lockb|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$' . gui
rg -n '"packageManager"|bun install|npm install' package.json gui/package.json

Repository: lidge-jun/opencodex

Length of output: 346


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- .github/dependabot.yml ---'
cat -n .github/dependabot.yml

printf '%s\n' '--- package manifests and lockfiles ---'
for f in package.json gui/package.json bun.lock gui/bun.lock docs-site/bun.lock; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    if [[ "$f" == *.json ]]; then
      cat -n "$f" | sed -n '1,90p'
    else
      sed -n '1,12p' "$f"
    fi
  fi
done

printf '%s\n' '--- dependency-install commands in automation and documentation ---'
rg -n --glob '!bun.lock' --glob '!**/node_modules/**' \
  'bun (install|add|update)|npm (install|ci)|package-lock\.json|bun\.lock|packageManager' \
  .github . 2>/dev/null | head -250

Repository: lidge-jun/opencodex

Length of output: 22479


🌐 Web query:

GitHub Dependabot supported ecosystems bun package-ecosystem bun current documentation

💡 Result:

GitHub Dependabot provides support for the Bun package manager using the package-ecosystem value bun in your dependabot.yml configuration file [1][2]. Key details regarding Bun support in Dependabot include: - Version Compatibility: Dependabot supports the Bun package manager for version 1.1.39 and above [1][2]. - Lockfile Support: Dependabot supports the current default text-based bun.lock file [1][2]. It does not support the legacy binary bun.lockb file [1][2]. - Feature Availability: Dependabot currently supports version updates for Bun [3]. As of the latest documentation, security updates for Bun are not listed as supported [4]. - Configuration: To use it, set the package-ecosystem to bun within your dependabot.yml file [1][5]: version: 2 updates: - package-ecosystem: "bun" directory: "/" schedule: interval: "daily" If you encounter issues with updates, ensure your project is using the supported text-based lockfile format and meets the minimum version requirements [1][5]. Some users have noted that enabling beta ecosystems (via enable-beta-ecosystems: true in the configuration) may occasionally be necessary for specific update behaviors, though it is not explicitly listed as a standard requirement in the official documentation for general Bun support [5].

Citations:


Change both Dependabot entries to the Bun ecosystem.

.github/dependabot.yml:6-7 and .github/dependabot.yml:25-26 manage directories that use text-based bun.lock files. CI runs bun install --frozen-lockfile, but the npm ecosystem does not update Bun lockfiles. Dependabot updates can therefore leave package.json and bun.lock inconsistent and cause CI failures.

Set both values to package-ecosystem: "bun". Add a separate Bun entry for /docs-site if its dependencies are in scope. This .github/** change also requires explicit security review and maintainer approval.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/dependabot.yml around lines 6 - 7, Update both Dependabot
configuration entries to use the "bun" package ecosystem instead of "npm",
preserving their existing directories; add a separate Bun entry for "/docs-site"
only if its dependencies are in scope. Ensure the .github configuration change
receives explicit security review and maintainer approval.

Sources: Path instructions, MCP tools

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +145 to +146
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- workflow context ---'
sed -n '110,215p' .github/workflows/ci.yml
printf '%s\n' '--- all checkout steps ---'
rg -n -A6 -B2 'uses:\s*actions/checkout@' .github/workflows/ci.yml
printf '%s\n' '--- workflow references ---'
rg -n 'persist-credentials|security|audit|bun|upload-artifact|actions/checkout' .github/workflows/ci.yml MAINTAINERS.md .github 2>/dev/null || true

Repository: lidge-jun/opencodex

Length of output: 18900


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- workflow header and permissions ---'
sed -n '1,115p' .github/workflows/ci.yml
printf '%s\n' '--- package install and lifecycle configuration ---'
sed -n '1,220p' package.json
printf '%s\n' '--- repository policy ---'
sed -n '20,75p' MAINTAINERS.md
printf '%s\n' '--- checkout hardening patterns ---'
sed -n '35,58p' .github/workflows/issue-quality-tests.yml
sed -n '20,45p' .github/workflows/pr-labeler.yml

Repository: lidge-jun/opencodex

Length of output: 10587


🌐 Web query:

actions/checkout persist-credentials default token stored local git config README

💡 Result:

The actions/checkout action uses the persist-credentials input to determine whether to configure the authentication token or SSH key with the local Git configuration [1][2]. When set to true (which is the default), this allows your scripts to execute authenticated Git commands [1][2][3]. The token or SSH key is automatically removed during the post-job cleanup phase [1][2][4]. Regarding where these credentials are stored: For versions prior to v6.0.0, the credentials were stored directly in the local Git configuration file (e.g.,.git/config) [5][6]. Starting with version v6.0.0, the action was updated to store the credentials in a separate file within the directory specified by the $RUNNER_TEMP environment variable instead of the local Git configuration file [3][6]. The Git configuration is then updated to include this temporary file, which improves security and helps prevent accidental credential leakage [3][6]. While there has been ongoing community discussion and requests to change the default value of persist-credentials to false for security hardening, it remains true by default as of August 2026 [7][3][8]. If you wish to opt out of this behavior, you can explicitly set persist-credentials: false in your workflow step [1][4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
sha='9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0'
url="https://raw.githubusercontent.com/actions/checkout/$sha/action.yml"
printf '%s\n' '--- pinned checkout action metadata ---'
curl --fail --silent --show-error "$url" | sed -n '1,160p'
printf '%s\n' '--- pinned checkout action commit ---'
curl --fail --silent --show-error \
  -H 'Accept: application/vnd.github+json' \
  "https://api.github.com/repos/actions/checkout/commits/$sha" |
  jq -r '[.sha, .commit.message] | `@tsv`'

Repository: lidge-jun/opencodex

Length of output: 5428


Disable persisted Git credentials in both checkout steps.

At .github/workflows/ci.yml:145-146 and :191-192, actions/checkout defaults persist-credentials to true. A subsequent action or command can then access the configured github.token. Add persist-credentials: false to both checkout steps.

🧰 Tools
🪛 zizmor (1.28.0)

[warning] 145-146: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 145 - 146, Update both
actions/checkout steps in the workflow to set persist-credentials to false,
including the checkout step near the existing “Checkout” name and the second
checkout step, so GitHub credentials are not retained after checkout.

Sources: Path instructions, MCP tools, Linters/SAST tools

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +156 to +178
- name: Generate Bun audit report
id: bun-audit
continue-on-error: true
shell: bash
run: |
set +e
# Full JSON evidence: `bun audit --json` ignores --audit-level, so this
# captures findings at every severity for the uploaded artifact.
bun audit --json > bun-audit.json
# Gate decision: bun audit only honors --audit-level in non-JSON mode,
# so derive the pass/fail from the human-readable run (high/critical only).
bun audit --audit-level=high
status=$?
echo "exit_code=$status" >> "$GITHUB_OUTPUT"
exit 0

- name: Upload Bun audit report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: bun-audit
path: bun-audit.json
retention-days: 7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

gh api repos/OnlineChefGroep/opencodex/actions/artifacts \
  --paginate \
  --jq '.artifacts[] | select(.name == "bun-audit") | [.id, .expired, .created_at] | `@tsv`'

Repository: lidge-jun/opencodex

Length of output: 2475


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- workflow context ---'
sed -n '130,190p' .github/workflows/ci.yml

printf '%s\n' '--- repository metadata ---'
gh api repos/OnlineChefGroep/opencodex \
  --jq '{full_name,visibility,private,default_branch}'

printf '%s\n' '--- workflow permissions and audit artifact references ---'
rg -n -C 3 'permissions:|upload-artifact|bun-audit|actions:read|contents:' .github/workflows/ci.yml .github 2>/dev/null || true

printf '%s\n' '--- unauthenticated repository metadata ---'
curl -fsSL https://api.github.com/repos/OnlineChefGroep/opencodex \
  | jq '{full_name,visibility,private,default_branch}' 2>/dev/null || true

Repository: lidge-jun/opencodex

Length of output: 15141


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- workflow triggers ---'
sed -n '1,55p' .github/workflows/ci.yml

printf '%s\n' '--- artifact metadata ---'
gh api repos/OnlineChefGroep/opencodex/actions/artifacts \
  --paginate \
  --jq '.artifacts[]
    | select(.name == "bun-audit")
    | {id, name, expired, created_at, workflow_run: .workflow_run.id}'

printf '%s\n' '--- audit report handling ---'
rg -n -C 4 'bun audit|bun-audit\.json|upload-artifact|retention-days' .github/workflows/ci.yml

Repository: lidge-jun/opencodex

Length of output: 9798


🌐 Web query:

GitHub Actions artifacts public repository who can download artifacts public repository read access documentation

💡 Result:

For public repositories on GitHub, anyone with read access to the repository can download workflow artifacts [1][2][3]. This includes individuals who can access the repository's main page or interact with its Actions workflow runs [2]. Key details regarding access and permissions: 1. Standard Access: Generally, if a repository is public, users with read access can view and download artifacts associated with workflow runs through the GitHub UI or the REST API [1][2][3]. 2. REST API: The GitHub REST API endpoints for artifacts (e.g., listing artifacts or downloading specific archives) explicitly state that anyone with read access to the repository can use these endpoints [1][3][4]. 3. Authentication for API usage: While read access is required, using the API may involve authentication requirements depending on the repository's visibility. For private repositories, OAuth tokens or personal access tokens (classic) with the repo scope are necessary [1][3]. For public repositories, while the documentation often notes that read access is sufficient, some users have reported that specific token scopes or authentication may be required in practice when using automated tools or curl to interact with the API [5]. 4. Downloading via Actions: By default, the actions/download-artifact action is scoped to the current workflow run [6][7]. If you need to download artifacts from a different repository or a different workflow run within a workflow, you must provide a github-token that has actions:read permissions on the target repository [6][7]. In summary, the ability to download artifacts from a public repository is tied to having read access to that repository [1][2]. If you have access to view the workflow run summary on GitHub, you generally have the permissions to download the associated artifacts [2].

Citations:


Keep the full Bun audit report out of public artifacts and logs.

This public workflow uploads all vulnerability severities for seven days and prints the full report when the audit fails. Keep only a redacted summary in the artifact and replace cat bun-audit.json with safe failure output. Store the full report in protected security tooling and remove existing non-expired bun-audit artifacts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 156 - 178, The Bun audit workflow
currently exposes full vulnerability details through the uploaded artifact and
failure output. Update the Generate Bun audit report and Upload Bun audit report
steps to retain only a redacted summary publicly, replace any failure-path cat
of bun-audit.json with safe non-sensitive output, route the full report to
protected security tooling, and remove existing non-expired bun-audit artifacts.

Sources: Path instructions, MCP tools

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +160 to +170
run: |
set +e
# Full JSON evidence: `bun audit --json` ignores --audit-level, so this
# captures findings at every severity for the uploaded artifact.
bun audit --json > bun-audit.json
# Gate decision: bun audit only honors --audit-level in non-JSON mode,
# so derive the pass/fail from the human-readable run (high/critical only).
bun audit --audit-level=high
status=$?
echo "exit_code=$status" >> "$GITHUB_OUTPUT"
exit 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- workflow context ---'
sed -n '130,185p' .github/workflows/ci.yml
printf '%s\n' '--- related audit references ---'
rg -n -C 3 'bun audit|exit_code|audit-level|bun-audit' .github MAINTAINERS.md 2>/dev/null || true

Repository: lidge-jun/opencodex

Length of output: 4171


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

path = Path(".github/workflows/ci.yml")
text = path.read_text()
block = text.split("      - name: Generate Bun audit report", 1)[1].split(
    "      - name: Upload Bun audit report", 1
)[0]

required = [
    "set +e",
    "bun audit --json > bun-audit.json",
    "bun audit --audit-level=high",
    'echo "exit_code=$status" >> "$GITHUB_OUTPUT"',
    "exit 0",
]
missing = [line for line in required if line not in block]
print("audit-step-lines:")
for number, line in enumerate(block.splitlines(), 156):
    print(f"{number}: {line}")
print("missing_expected_lines:", missing)

# Check the data flow into the enforcement condition.
condition = re.search(
    r"if: steps\.bun-audit\.outputs\.exit_code != '0'", text
)
print("enforcement_uses_human_audit_output:", bool(condition))
print("json_status_captured:", bool(re.search(
    r"bun audit --json[^\n]*\n(?:(?!bun audit --audit-level).)*"
    r"(?:status|PIPESTATUS|json).*GITHUB_OUTPUT",
    block,
    re.S,
)))
PY

printf '%s\n' '--- relevant workflow metadata ---'
sed -n '145,185p' .github/workflows/ci.yml

Repository: lidge-jun/opencodex

Length of output: 2360


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

text = Path(".github/workflows/ci.yml").read_text()
start = text.index("      - name: Generate Bun audit report")
end = text.index("      - name: Upload Bun audit report", start)
lines = text[start:end].splitlines()

json_idx = next(i for i, line in enumerate(lines) if "bun audit --json" in line)
human_idx = next(i for i, line in enumerate(lines) if "bun audit --audit-level=high" in line)
status_idx = next(i for i, line in enumerate(lines) if "status=$?" in line)
output_idx = next(i for i, line in enumerate(lines) if "exit_code=$status" in line)

print("json_command_line:", json_idx + 1)
print("human_command_line:", human_idx + 1)
print("status_capture_line:", status_idx + 1)
print("output_line:", output_idx + 1)
print("json_status_capture_before_human:", any(
    "status=$?" in line or "PIPESTATUS" in line
    for line in lines[json_idx + 1:human_idx]
))
print("status_captured_after_human:", status_idx > human_idx)
print("output_uses_human_status:", output_idx > status_idx > human_idx)

# Abstract shell-status simulation for the exact data flow:
for json_status, human_status in ((1, 0), (0, 1), (1, 1)):
    # The workflow does not save json_status; status is assigned only after human audit.
    emitted = human_status
    print(f"json_status={json_status}, human_status={human_status}, emitted_exit_code={emitted}")
PY

Repository: lidge-jun/opencodex

Length of output: 499


Fail closed when the JSON audit report fails

At .github/workflows/ci.yml:164, the workflow discards the status from bun audit --json. If report generation fails while the high-severity audit returns zero, the workflow emits exit_code=0 and passes without valid audit evidence.

Capture and validate the JSON report separately. Use the human-readable audit status only for the high-severity vulnerability gate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 160 - 170, Update the audit step
around the JSON and human-readable bun audit commands to capture the JSON report
command’s exit status separately and fail the workflow when report generation
fails, even if the high-severity audit passes. Preserve the human-readable audit
status as the value written to GITHUB_OUTPUT for the vulnerability gate, while
ensuring invalid or missing JSON evidence cannot produce a successful overall
step.

Comment on lines +113 to 136
# All releases run from main. Stable versions (no pre-release suffix) get dist-tag "latest".
# Pre-release versions (e.g. 1.0.0-alpha.1) get dist-tag "preview".
case "$GITHUB_REF" in
refs/heads/main)
expected_tag="latest"
if [[ "$RELEASE_VERSION" == *-* ]]; then
echo "::error::main releases must use a stable semver version; got ${RELEASE_VERSION}"
exit 1
fi
;;
refs/heads/preview)
expected_tag="preview"
if [[ "$RELEASE_VERSION" != *-preview.* ]]; then
echo "::error::preview releases must use a preview prerelease version; got ${RELEASE_VERSION}"
exit 1
# Pre-release version — must use "preview" dist-tag
if [ "$NPM_DIST_TAG" != "preview" ]; then
echo "::error::Pre-release versions (${RELEASE_VERSION}) must use dist-tag 'preview', got '${NPM_DIST_TAG}'"
exit 1
fi
else
# Stable version — must use "latest" dist-tag
if [ "$NPM_DIST_TAG" != "latest" ]; then
echo "::error::Stable releases (${RELEASE_VERSION}) must use dist-tag 'latest', got '${NPM_DIST_TAG}'"
exit 1
fi
fi
;;
*)
echo "::error::Release must run from main or preview; got ${GITHUB_REF}"
echo "::error::Release must run from main; got ${GITHUB_REF}"
exit 1
;;
esac

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 3 \
  'RELEASE_VERSION|preview|prerelease|matching-preview-tags' \
  .github/workflows/release.yml scripts/release-notes.ts scripts/release.ts

Repository: lidge-jun/opencodex

Length of output: 24314


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- release workflow ---'
sed -n '145,175p;294,325p;337,352p' .github/workflows/release.yml

printf '%s\n' '--- release helper ---'
sed -n '215,250p' scripts/release.ts

printf '%s\n' '--- release-notes helper ---'
sed -n '14,35p' scripts/release-notes.ts

printf '%s\n' '--- declared release version ---'
sed -n '1,25p' CHANGELOG.md

printf '%s\n' '--- all release predicates ---'
rg -n 'includes\("-"\)|\*-|preview\.|prerelease|NPM_DIST_TAG' \
  .github/workflows/release.yml scripts/release-notes.ts scripts/release.ts

Repository: lidge-jun/opencodex

Length of output: 10507


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- workflow inputs and release trigger ---'
sed -n '1,45p;80,145p' .github/workflows/release.yml

printf '%s\n' '--- versioning policy ---'
rg -n -C 4 'alpha|beta|preview|main|stable|prerelease' VERSIONING.md README.md CHANGELOG.md scripts/release.ts

printf '%s\n' '--- deterministic predicate probe ---'
RELEASE_VERSIONS=('1.0.0' '1.0.0-alpha.1' '1.0.0-beta.1' '1.0.0-rc.1' '1.0.0-preview.1')
for version in "${RELEASE_VERSIONS[@]}"; do
  if [[ "$version" == *-* ]]; then
    validation_channel='preview'
    github_prerelease='yes'
  else
    validation_channel='latest'
    github_prerelease='no'
  fi
  if [[ "$version" == *-preview.* ]]; then
    history_channel='preview'
    carry_mode='stable-only'
  else
    history_channel='stable'
    carry_mode='matching-preview-tags'
  fi
  printf '%-20s validation=%-7s github_prerelease=%-3s history=%-6s carry=%s\n' \
    "$version" "$validation_channel" "$github_prerelease" "$history_channel" "$carry_mode"
done

Repository: lidge-jun/opencodex

Length of output: 24253


Keep prerelease classification consistent.

release.yml:117 and release.yml:321 classify 1.0.0-alpha.1, 1.0.0-beta.1, and 1.0.0-rc.1 as preview releases. The history and carry logic at release.yml:156-168, release.yml:301-315, and release.yml:344-348 recognizes only -preview.*. These versions therefore receive preview metadata but use the stable history baseline and cannot participate in preview-note carry. This conflicts with CHANGELOG.md:8 and CHANGELOG.md:13.

Use one prerelease predicate across the workflow and scripts/release-notes.ts, or reject all prerelease identifiers except -preview.*.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 113 - 136, Unify prerelease
detection across the release workflow and scripts/release-notes.ts so alpha,
beta, rc, and preview versions follow the same preview history and carry-note
behavior. Update the classification and related history/carry logic around
NPM_DIST_TAG, RELEASE_VERSION, and release-notes handling to use one shared
predicate, or consistently reject non-preview prerelease identifiers.

Comment thread src/usage/budgets.ts
Comment on lines +90 to +108
try {
if (!existsSync(this.statePath)) return fresh;
const parsed = JSON.parse(readFileSync(this.statePath, "utf-8")) as PersistedState;
if (!parsed || typeof parsed !== "object") return fresh;
// Roll over windows if we've crossed a boundary.
if (startOfDay(now) !== parsed.dayStart) {
parsed.todayTokens = 0;
parsed.todayCostEur = 0;
parsed.dayStart = startOfDay(now);
}
if (startOfWeek(now) !== parsed.weekStart) {
parsed.weekTokens = 0;
parsed.weekStart = startOfWeek(now);
}
return { ...fresh, ...parsed };
} catch {
return fresh;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate persisted field types before merging into state.

load() parses budget-state.json and merges it with { ...fresh, ...parsed } (Line 104) without checking that parsed.todayTokens, parsed.weekTokens, and parsed.todayCostEur are actually numbers. If the persisted file ever contains a non-numeric value for one of these fields (partial write, manual edit, future schema change), recordUsage() (Line 145-147) performs this.state.todayTokens += tokens on a non-number, silently producing string concatenation instead of numeric accumulation, and all subsequent threshold checks become meaningless without ever throwing.

Add a type guard when merging the persisted counters.

🛠️ Proposed fix
-      return { ...fresh, ...parsed };
+      return {
+        dayStart: parsed.dayStart,
+        weekStart: parsed.weekStart,
+        todayTokens: typeof parsed.todayTokens === "number" ? parsed.todayTokens : fresh.todayTokens,
+        todayCostEur: typeof parsed.todayCostEur === "number" ? parsed.todayCostEur : fresh.todayCostEur,
+        weekTokens: typeof parsed.weekTokens === "number" ? parsed.weekTokens : fresh.weekTokens,
+      };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
if (!existsSync(this.statePath)) return fresh;
const parsed = JSON.parse(readFileSync(this.statePath, "utf-8")) as PersistedState;
if (!parsed || typeof parsed !== "object") return fresh;
// Roll over windows if we've crossed a boundary.
if (startOfDay(now) !== parsed.dayStart) {
parsed.todayTokens = 0;
parsed.todayCostEur = 0;
parsed.dayStart = startOfDay(now);
}
if (startOfWeek(now) !== parsed.weekStart) {
parsed.weekTokens = 0;
parsed.weekStart = startOfWeek(now);
}
return { ...fresh, ...parsed };
} catch {
return fresh;
}
}
try {
if (!existsSync(this.statePath)) return fresh;
const parsed = JSON.parse(readFileSync(this.statePath, "utf-8")) as PersistedState;
if (!parsed || typeof parsed !== "object") return fresh;
// Roll over windows if we've crossed a boundary.
if (startOfDay(now) !== parsed.dayStart) {
parsed.todayTokens = 0;
parsed.todayCostEur = 0;
parsed.dayStart = startOfDay(now);
}
if (startOfWeek(now) !== parsed.weekStart) {
parsed.weekTokens = 0;
parsed.weekStart = startOfWeek(now);
}
return {
dayStart: parsed.dayStart,
weekStart: parsed.weekStart,
todayTokens: typeof parsed.todayTokens === "number" ? parsed.todayTokens : fresh.todayTokens,
todayCostEur: typeof parsed.todayCostEur === "number" ? parsed.todayCostEur : fresh.todayCostEur,
weekTokens: typeof parsed.weekTokens === "number" ? parsed.weekTokens : fresh.weekTokens,
};
} catch {
return fresh;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/usage/budgets.ts` around lines 90 - 108, Update the load() method to
validate that parsed.todayTokens, parsed.weekTokens, and parsed.todayCostEur are
numbers before merging persisted data with fresh. If any counter has an invalid
type, retain the corresponding fresh/default value while preserving valid
persisted fields and the existing rollover behavior.

Comment thread src/usage/budgets.ts
Comment on lines +241 to +246
export function getBudgetTracker(config?: BudgetConfig): BudgetTracker {
if (!cachedTracker) {
cachedTracker = new BudgetTracker(config ?? {});
}
return cachedTracker;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard getBudgetTracker() against construction failures, matching the posthog singleton pattern.

getBudgetTracker() calls new BudgetTracker(config ?? {}) with no try/catch. The constructor (lines 74-79) calls getConfigDir() and, via load(), filesystem I/O. If getConfigDir() or the underlying I/O throws for any environmental reason (permissions, read-only filesystem, disk full), the exception propagates to the first caller of getBudgetTracker() — likely on the first recordUsage() call in a request path — rather than degrading gracefully.

Compare this with getServerPosthog() in src/telemetry/posthog-server.ts (lines 167-182), which wraps construction in try/catch and returns null on any failure. Apply the same defensive pattern here so a budget-tracking failure never breaks request handling, consistent with this file's own "best-effort" design elsewhere (Line 116, Line 219).

🛠️ Proposed fix
 export function getBudgetTracker(config?: BudgetConfig): BudgetTracker {
   if (!cachedTracker) {
-    cachedTracker = new BudgetTracker(config ?? {});
+    try {
+      cachedTracker = new BudgetTracker(config ?? {});
+    } catch {
+      // Never let budget tracking construction break the caller.
+      cachedTracker = new BudgetTracker({});
+    }
   }
   return cachedTracker;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/usage/budgets.ts` around lines 241 - 246, Update getBudgetTracker to wrap
new BudgetTracker(config ?? {}) in a try/catch, matching the defensive
construction pattern used by getServerPosthog. On construction failure, avoid
propagating the exception and return the existing nullable/disabled tracker
result expected by callers, while preserving cachedTracker behavior on
successful construction.

Comment thread src/usage/percentiles.ts
Comment on lines +50 to +64
export function computeLatencyStats(samples: number[]): LatencyStats {
if (samples.length === 0) {
return { count: 0, p50: 0, p95: 0, p99: 0, min: 0, max: 0, mean: 0 };
}
const sum = samples.reduce((a, b) => a + b, 0);
return {
count: samples.length,
p50: Math.round(percentile(samples, 50)),
p95: Math.round(percentile(samples, 95)),
p99: Math.round(percentile(samples, 99)),
min: Math.min(...samples),
max: Math.max(...samples),
mean: Math.round(sum / samples.length),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether callers bound the size of the `samples`/log arrays passed into
# computeLatencyStats / groupByProvider before invocation.
rg -n -C6 'computeLatencyStats|groupByProvider' --type=ts -g '!tests/*'

Repository: lidge-jun/opencodex

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)percentiles\.ts$|usage|latency|provider'
printf '%s\n' '--- symbol references ---'
rg -n -C4 'computeLatencyStats|groupByProvider|percentile\s*\(' --glob '*.ts' --glob '*.tsx' --glob '!tests/**' . || true
printf '%s\n' '--- file outline ---'
ast-grep outline src/usage/percentiles.ts --view expanded
printf '%s\n' '--- source ---'
cat -n src/usage/percentiles.ts

Repository: lidge-jun/opencodex

Length of output: 31478


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- imports and endpoint references ---'
rg -n -C6 'usage/percentiles|latency-stats|groupByProvider|computeLatencyStats|percentile\(' \
  src gui tests --glob '*.ts' --glob '*.tsx' || true
printf '%s\n' '--- usage-related route implementation ---'
cat -n src/server/management/logs-usage-routes.ts
printf '%s\n' '--- usage log implementation ---'
cat -n src/usage/log.ts

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -e
node - <<'JS'
const sizes = [100_000, 125_000, 126_000, 200_000];
for (const size of sizes) {
  const values = new Array(size).fill(1);
  let result = "ok";
  try {
    Math.min(...values);
    Math.max(...values);
  } catch (error) {
    result = `${error.name}: ${error.message}`;
  }
  console.log(`${size}: ${result}`);
}

function percentileFromSorted(sorted, p) {
  if (sorted.length === 0) return 0;
  if (sorted.length === 1) return sorted[0];
  const clampedP = Math.max(0, Math.min(100, p));
  if (clampedP === 0) return sorted[0];
  if (clampedP === 100) return sorted[sorted.length - 1];
  const rank = (clampedP / 100) * (sorted.length - 1);
  const lo = Math.floor(rank);
  const hi = Math.ceil(rank);
  if (lo === hi) return sorted[lo];
  return sorted[lo] + (sorted[hi] - sorted[lo]) * (rank - lo);
}

const samples = [500, 100, 400, 200, 300];
const sorted = [...samples].sort((a, b) => a - b);
console.log(JSON.stringify({
  p50: percentileFromSorted(sorted, 50),
  p95: percentileFromSorted(sorted, 95),
  p99: percentileFromSorted(sorted, 99),
  min: sorted[0],
  max: sorted[sorted.length - 1],
}));
JS

Repository: lidge-jun/opencodex

Length of output: 379


Reuse one sorted copy in src/usage/percentiles.ts:50-64.

computeLatencyStats() sorts samples three times through percentile(). Math.min(...samples) and Math.max(...samples) can also throw RangeError for large inputs. The /api/latency-stats path is not currently wired in this repository, so this is a defensive utility refactor.

Sort once, use a percentileFromSorted() helper, and read min and max from the sorted array. Keep percentile() as the unsorted-input wrapper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/usage/percentiles.ts` around lines 50 - 64, Update computeLatencyStats to
create one sorted copy of samples, use a percentileFromSorted helper for p50,
p95, and p99, and derive min and max from the sorted array to avoid repeated
sorting and spread-argument limits. Preserve percentile as the wrapper for
unsorted inputs by having it sort a copy and delegate to percentileFromSorted.

Comment thread src/usage/pricing.ts
Comment on lines +71 to +85
/** Find the best-matching pricing entry: model-specific, then provider, then free. */
export function lookupPricing(provider: string, model?: string): ProviderPricing {
if (model) {
const modelKey = `${provider}:${model.toLowerCase()}`;
if (PRICING[modelKey]) return PRICING[modelKey];
// Try a prefix match (e.g. "claude-sonnet-4-5-20250929" → "claude-sonnet-4-5")
const prefixHit = Object.keys(PRICING).find((k) => {
if (!k.startsWith(`${provider}:`)) return false;
const baseModel = k.slice(provider.length + 1);
return baseModel !== provider && model.toLowerCase().startsWith(baseModel);
});
if (prefixHit) return PRICING[prefixHit];
}
return PRICING[provider] ?? { inputPer1k: 0, outputPer1k: 0 };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prefix-match picks the first key, not the most specific one — this misprices gpt-4o-mini-family models.

In lookupPricing(), the prefix fallback (lines 77-82) uses Object.keys(PRICING).find(...), which returns the first matching entry in object-declaration order. "openai:gpt-4o" is declared before "openai:gpt-4o-mini" (Line 23 vs Line 25). Since "gpt-4o" is a literal string prefix of "gpt-4o-mini", any real-world versioned model id such as "gpt-4o-mini-2024-07-18" (OpenAI's typical dated model-id format) matches "openai:gpt-4o" first and never reaches the "openai:gpt-4o-mini" entry.

Concretely: lookupPricing("openai", "gpt-4o-mini-2024-07-18") returns { inputPer1k: 0.0023, outputPer1k: 0.0092 } (the full gpt-4o price) instead of { inputPer1k: 0.00013, outputPer1k: 0.00052 } (the mini price) — roughly 18x too expensive. This cost estimate feeds estimateCostEur() and then BudgetTracker.recordUsage()'s daily cost threshold, so this can trigger false cost-daily budget alerts for anyone routing gpt-4o-mini traffic.

Pick the longest (most specific) matching key instead of the first one found.

🛠️ Proposed fix
   if (model) {
     const modelKey = `${provider}:${model.toLowerCase()}`;
     if (PRICING[modelKey]) return PRICING[modelKey];
     // Try a prefix match (e.g. "claude-sonnet-4-5-20250929" → "claude-sonnet-4-5")
-    const prefixHit = Object.keys(PRICING).find((k) => {
-      if (!k.startsWith(`${provider}:`)) return false;
-      const baseModel = k.slice(provider.length + 1);
-      return baseModel !== provider && model.toLowerCase().startsWith(baseModel);
-    });
+    const prefixHit = Object.keys(PRICING)
+      .filter((k) => {
+        if (!k.startsWith(`${provider}:`)) return false;
+        const baseModel = k.slice(provider.length + 1);
+        return model.toLowerCase().startsWith(baseModel);
+      })
+      // Prefer the longest (most specific) matching base model.
+      .sort((a, b) => b.length - a.length)[0];
     if (prefixHit) return PRICING[prefixHit];
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Find the best-matching pricing entry: model-specific, then provider, then free. */
export function lookupPricing(provider: string, model?: string): ProviderPricing {
if (model) {
const modelKey = `${provider}:${model.toLowerCase()}`;
if (PRICING[modelKey]) return PRICING[modelKey];
// Try a prefix match (e.g. "claude-sonnet-4-5-20250929" → "claude-sonnet-4-5")
const prefixHit = Object.keys(PRICING).find((k) => {
if (!k.startsWith(`${provider}:`)) return false;
const baseModel = k.slice(provider.length + 1);
return baseModel !== provider && model.toLowerCase().startsWith(baseModel);
});
if (prefixHit) return PRICING[prefixHit];
}
return PRICING[provider] ?? { inputPer1k: 0, outputPer1k: 0 };
}
/** Find the best-matching pricing entry: model-specific, then provider, then free. */
export function lookupPricing(provider: string, model?: string): ProviderPricing {
if (model) {
const modelKey = `${provider}:${model.toLowerCase()}`;
if (PRICING[modelKey]) return PRICING[modelKey];
// Try a prefix match (e.g. "claude-sonnet-4-5-20250929" → "claude-sonnet-4-5")
const prefixHit = Object.keys(PRICING)
.filter((k) => {
if (!k.startsWith(`${provider}:`)) return false;
const baseModel = k.slice(provider.length + 1);
return model.toLowerCase().startsWith(baseModel);
})
// Prefer the longest (most specific) matching base model.
.sort((a, b) => b.length - a.length)[0];
if (prefixHit) return PRICING[prefixHit];
}
return PRICING[provider] ?? { inputPer1k: 0, outputPer1k: 0 };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/usage/pricing.ts` around lines 71 - 85, Update the prefix fallback in
lookupPricing to select the longest matching provider model key rather than the
first Object.keys(PRICING) match, ensuring specific entries such as gpt-4o-mini
take precedence over gpt-4o while preserving the existing exact-match and
provider fallback behavior.

Comment thread VERSIONING.md Outdated
Comment on lines +24 to +33
## Current version

Current: **`2.7.33`** (inherited from upstream; transition starts here)

Next release: **`1.0.0`** — our first independent release, signifying the fork's new identity.

## Version transition

Because this fork initially tracked upstream releases, the existing tags (`v2.7.26` … `v2.7.39`)
are kept for history. All **new** releases use our own scheme starting at `v1.0.0`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Correct the version transition before release.

Current: 2.7.33 differs from the supplied package.json version 2.7.42 and is older than this file’s retained v2.7.39 history.

Next release: 1.0.0 conflicts with CHANGELOG.md Line 13 and ROADMAP.md Line 21, which identify 1.0.0-alpha.1 as the first fork release.

Choose one transition policy. If alpha.1 precedes stable 1.0.0, document both milestones explicitly and update the current version from the package manifest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@VERSIONING.md` around lines 24 - 33, Update the version transition section in
VERSIONING.md to use a consistent policy: set Current to the version from
package.json (2.7.42), document 1.0.0-alpha.1 as the first fork release, and
explicitly identify 1.0.0 as the subsequent stable milestone. Align the
surrounding release-history wording with this sequence.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines +33 to +45
function normalizeFallback(raw: ProviderFallbackTarget[] | undefined): ProviderFallbackTarget[] {
if (!Array.isArray(raw)) return [];
return raw
.map(row => ({
provider: typeof row.provider === "string" ? row.provider.trim() : "",
model: typeof row.model === "string" ? row.model.trim() : "",
}))
.filter(row => row.provider && row.model);
}

function fallbackFingerprint(rows: ProviderFallbackTarget[]): string {
return JSON.stringify(normalizeFallback(rows));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate file outline ---'
ast-grep outline gui/src/components/provider-workspace/ProviderSettings.tsx --match 'function normalizeFallback' --view expanded || true
printf '%s\n' '--- ProviderSettings.tsx relevant sections ---'
sed -n '1,220p' gui/src/components/provider-workspace/ProviderSettings.tsx
sed -n '300,420p' gui/src/components/provider-workspace/ProviderSettings.tsx
printf '%s\n' '--- fallback validation/runtime references ---'
rg -n -A35 -B10 'providerFallbackIssues|providerFallbackPlan|usable\\(' src gui
printf '%s\n' '--- fallback construction and WorkspaceItem references ---'
rg -n -A12 -B12 'WorkspaceItem|fallback|config\\.providers|Providers' gui/src src | head -n 800

Repository: lidge-jun/opencodex

Length of output: 15043


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- ProviderSettings.tsx middle and exact line ranges ---'
sed -n '210,370p' gui/src/components/provider-workspace/ProviderSettings.tsx
printf '%s\n' '--- type declarations and parent usage ---'
rg -n -A25 -B10 'export type WorkspaceItem|type WorkspaceItem|ProviderSettings|peerProviders|fallback:' gui/src
printf '%s\n' '--- fallback server symbols ---'
rg -n -A35 -B12 'providerFallbackIssues|providerFallbackPlan|usable' src gui
printf '%s\n' '--- provider config/refetch paths ---'
rg -n -A25 -B15 'config\.providers|providers\[|fallback' gui/src | head -n 1200

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- exact fallback symbol files ---'
rg -l 'providerFallbackIssues|providerFallbackPlan' src gui
printf '%s\n' '--- exact WorkspaceItem declarations/usages ---'
rg -l 'WorkspaceItem' gui/src
printf '%s\n' '--- focused type and component references ---'
rg -n -A18 -B8 'WorkspaceItem|ProviderSettings' gui/src/components/provider-workspace gui/src | head -n 500

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- WorkspaceItem definition and catalog mapping ---'
rg -n -A35 -B12 'export (type|interface) WorkspaceItem|type WorkspaceItem|fallback' gui/src/provider-workspace/catalog.ts
printf '%s\n' '--- Providers shell data flow and update callback ---'
rg -n -A28 -B12 'onUpdateProvider|peerProviders|catalog|fetch.*provider|set.*providers|reload|refresh' gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx gui/src/provider-workspace/catalog.ts
printf '%s\n' '--- server fallback implementation ---'
sed -n '1,210p' src/providers/fallback.ts
printf '%s\n' '--- config provider patch validation/update paths ---'
rg -n -A45 -B15 'fallback|PATCH.*providers|onUpdateProvider|updateProvider' src/config.ts gui/src

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- catalog structure ---'
wc -l gui/src/provider-workspace/catalog.ts gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx src/providers/fallback.ts src/config.ts
rg -n 'fallback|Object\.entries|WorkspaceItem|export function|fetch|providers' gui/src/provider-workspace/catalog.ts gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
printf '%s\n' '--- server fallback implementation ---'
sed -n '1,190p' src/providers/fallback.ts
printf '%s\n' '--- config fallback references ---'
rg -n -A18 -B12 'fallback' src/config.ts

Repository: lidge-jun/opencodex

Length of output: 18792


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- catalog item creation ---'
sed -n '190,255p' gui/src/provider-workspace/catalog.ts
printf '%s\n' '--- ProviderWorkspaceShell provider prop flow ---'
sed -n '45,120p' gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx
printf '%s\n' '--- parent files that render ProviderWorkspaceShell ---'
rg -l 'ProviderWorkspaceShell' gui/src
printf '%s\n' '--- provider fetch/update references in those parents ---'
for f in $(rg -l 'ProviderWorkspaceShell' gui/src); do
  echo "FILE: $f"
  rg -n -A30 -B15 'ProviderWorkspaceShell|/api/config|/api/providers|providers:' "$f"
done

Repository: lidge-jun/opencodex

Length of output: 16180


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Providers hook imports and state wiring ---'
sed -n '1,180p' gui/src/pages/Providers.tsx
printf '%s\n' '--- provider fetch hook ---'
sed -n '1,260p' gui/src/pages/use-providers-fetch.ts
printf '%s\n' '--- provider CRUD hook ---'
sed -n '1,320p' gui/src/pages/use-providers-crud.ts

Repository: lidge-jun/opencodex

Length of output: 15819


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- exact review locations ---'
nl -ba gui/src/components/provider-workspace/ProviderSettings.tsx | sed -n '30,48p;84,145p;168,202p;315,395p'
printf '%s\n' '--- exact upstream refetch locations ---'
nl -ba gui/src/pages/use-providers-fetch.ts | sed -n '12,35p'
nl -ba gui/src/pages/use-providers-crud.ts | sed -n '70,125p'
printf '%s\n' '--- standalone behavior verifier ---'
node - <<'JS'
function normalizeFallback(raw) {
  if (!Array.isArray(raw)) return [];
  return raw
    .map(row => ({
      provider: typeof row.provider === "string" ? row.provider.trim() : "",
      model: typeof row.model === "string" ? row.model.trim() : "",
    }))
    .filter(row => row.provider && row.model);
}
function fallbackFingerprint(rows) {
  return JSON.stringify(normalizeFallback(rows));
}
function fallbackDirtyKey(rows) {
  return JSON.stringify(rows
    .map(row => ({ provider: row.provider.trim(), model: row.model.trim() }))
    .filter(row => row.provider || row.model));
}
const original = [];
const partial = [{ provider: "peer-without-model-list", model: "" }];
console.log(JSON.stringify({
  fingerprint_original: fallbackFingerprint(original),
  fingerprint_partial: fallbackFingerprint(partial),
  fingerprint_equal: fallbackFingerprint(original) === fallbackFingerprint(partial),
  dirty_key_original: fallbackDirtyKey(original),
  dirty_key_partial: fallbackDirtyKey(partial),
  dirty_key_equal: fallbackDirtyKey(original) === fallbackDirtyKey(partial),
}));
const first = JSON.parse('{"providers":{"p":{"fallback":[{"provider":"q","model":"m"}]}}}');
const second = JSON.parse('{"providers":{"p":{"fallback":[{"provider":"q","model":"m"}]}}}');
console.log(JSON.stringify({
  same_value: JSON.stringify(first.providers.p.fallback) === JSON.stringify(second.providers.p.fallback),
  same_reference: Object.is(first.providers.p.fallback, second.providers.p.fallback),
}));
JS

Repository: lidge-jun/opencodex

Length of output: 631


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- exact line numbers without nl ---'
awk 'NR>=10 && NR<=32 {print NR ":" $0}' gui/src/pages/use-providers-fetch.ts
awk 'NR>=65 && NR<=100 {print NR ":" $0}' gui/src/pages/use-providers-crud.ts
awk 'NR>=320 && NR<=395 {print NR ":" $0}' gui/src/components/provider-workspace/ProviderSettings.tsx

Repository: lidge-jun/opencodex

Length of output: 6167


Fix fallback dirty tracking and invalid target selection

  • fallbackFingerprint drops partially filled rows. Therefore, dirty remains false for a row with only a provider or model, and the sticky Save/Discard bar does not render at gui/src/components/provider-workspace/ProviderSettings.tsx:384. Add a dirty-specific key that retains rows where either field is non-empty. Keep normalizeFallback for the save payload so the existing pws.fallbackIncomplete validation runs.
  • item.fallback is a reference dependency at gui/src/components/provider-workspace/ProviderSettings.tsx:99. useProvidersFetch.fetchConfig assigns freshly parsed /api/config data at gui/src/pages/use-providers-fetch.ts:23-27, so an equal fallback array has a new reference after each refetch. The reset effect can then discard unrelated edits. Use a value-based fallback key in this dependency.
  • At gui/src/components/provider-workspace/ProviderSettings.tsx:334-337, disabled providers are labeled but remain selectable. providerFallbackPlan skips disabled targets at runtime. Disable or filter these options.
  • Prevent duplicate {provider, model} pairs before saving. providerFallbackIssues rejects duplicates, while useProvidersCrud.ts:81-83 returns the raw server error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/components/provider-workspace/ProviderSettings.tsx` around lines 33 -
45, Update fallback handling in ProviderSettings: add a dirty-tracking
fingerprint that preserves rows with either non-empty provider or model while
keeping normalizeFallback for the save payload and validation; replace the
item.fallback reference dependency in the reset effect with this value-based
key; disable or filter disabled providers from fallback selection options; and
deduplicate provider/model pairs before saving so providerFallbackIssues cannot
reject duplicates.

Comment on lines +322 to +339
<div key={index} className="pwi-fallback-row">
<select
className="input"
value={row.provider}
aria-label={t("pws.fallback.provider")}
onChange={e => {
const provider = e.target.value;
const first = modelsForPeer(fallbackPeers.find(p => p.name === provider), "")[0] ?? "";
updateFallbackRow(index, { provider, model: first });
}}
>
<option value="">{t("pws.fallback.pickProvider")}</option>
{fallbackPeers.map(p => (
<option key={p.name} value={p.name}>
{p.disabled ? t("pws.fallback.disabled", { name: p.name }) : p.name}
</option>
))}
</select>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fallback picker lets users pick disabled providers and duplicate targets without warning.

Line 336 shows a disabled peer with a distinct label (pws.fallback.disabled) but still lets it be selected as a normal <option>. At runtime, providerFallbackPlan.usable() (src/providers/fallback.ts, Line 145-149) silently drops disabled targets from the synthetic combo with no log or user-facing signal — a saved fallback pointing only at disabled providers will never trigger and nothing tells the operator why.

Separately, nothing here prevents selecting the same provider+model pair in two rows. The server's providerFallbackIssues (src/providers/fallback.ts, Line 112-117) rejects duplicates with a hard 400 error, and save() (Line 192) surfaces that raw, untranslated server message directly to the user instead of a localized GUI string.

Filter fallbackPeers/modelIds to exclude already-disabled providers and already-selected {provider, model} pairs from subsequent rows, so the UI cannot construct a config the server will reject or silently ignore.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/components/provider-workspace/ProviderSettings.tsx` around lines 322
- 339, Update the fallback-row rendering and selection logic in ProviderSettings
so disabled fallbackPeers cannot be selected and subsequent rows exclude
provider/model pairs already selected in earlier rows. Filter the available
providers and modelIds using the current row’s value plus prior fallback rows,
while preserving the current row’s existing selection so it remains visible;
ensure the resulting UI cannot save disabled targets or duplicate provider/model
combinations.

Comment on lines +141 to +158
<div className="pws-usage-block-head">
<h3 className="pws-section-title">{t("pws.rateLimits")}</h3>
{onRefreshQuota && (
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={onRefreshQuota}
disabled={quotaRefreshing}
aria-label={t("prov.quotaRefreshAria", { name: item.name })}
>
{quotaRefreshing ? <span className="spin" /> : <IconRefresh />}
{quotaRefreshing ? t("prov.quotaRefreshing") : quotaFailed ? t("pws.retry") : t("prov.quotaRefresh")}
</button>
)}
</div>
{quotaFailed && (
<p className="text-caption" style={{ color: "var(--amber)" }} role="status">{t("prov.quotaRefreshFailed")}</p>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Missing translation keys used by this refresh button.

Line 149 calls t("prov.quotaRefreshAria", { name: item.name }). Line 152 calls t("prov.quotaRefreshing") and t("prov.quotaRefresh"). Line 157 calls t("prov.quotaRefreshFailed").

gui/src/i18n/en.ts does not define any of these 4 keys. gui/src/i18n/de.ts, ko.ts, and zh.ts do not define them either. Only gui/src/i18n/ja.ts (lines 306-309) and gui/src/i18n/ru.ts (lines 313-316) define them.

For English, German, Korean, and Chinese users, this button renders with a missing translation (empty or fallback text) for its label and its aria-label. For Japanese and Russian users, if TKey is derived strictly from en.ts (as the de.ts header comment states: "Must match TKey set (compile-checked)"), ja.ts and ru.ts fail to type-check because they assign extra properties not present in TKey.

Add prov.quotaRefresh, prov.quotaRefreshing, prov.quotaRefreshFailed, and prov.quotaRefreshAria to en.ts first, then mirror them into de.ts, ko.ts, and zh.ts using the same wording pattern already used in ja.ts/ru.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/components/provider-workspace/ProviderUsage.tsx` around lines 141 -
158, Add the four quota refresh translation keys—prov.quotaRefresh,
prov.quotaRefreshing, prov.quotaRefreshFailed, and prov.quotaRefreshAria—to the
English translation definitions in en.ts, then add matching localized entries to
de.ts, ko.ts, and zh.ts following the existing ja.ts and ru.ts wording pattern
so ProviderUsage’s refresh button and failure message are translated and all
locale types remain consistent.

Source: Path instructions

Comment thread gui/src/i18n/en.ts
Comment on lines +864 to +873
"pws.fallback": "Fallback providers",
"pws.fallbackDesc": "On retryable failures (429, 5xx, stream drop), hop to these targets in order. Leave empty to return the error to the client.",
"pws.fallback.add": "Add fallback",
"pws.fallback.provider": "Fallback provider",
"pws.fallback.model": "Fallback model",
"pws.fallback.pickProvider": "Pick provider",
"pws.fallback.pickModel": "Pick model",
"pws.fallback.modelPlaceholder": "model id",
"pws.fallback.disabled": "{name} (disabled)",
"pws.fallbackIncomplete": "Each fallback row needs both a provider and a model.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
git ls-files 'gui/src/i18n/*' 'gui/src/components/provider-workspace/ProviderUsage.tsx' 'gui/src/*' | sed -n '1,120p'
printf '%s\n' '--- locale declarations and key usage ---'
rg -n --glob 'gui/src/i18n/*.ts' --glob 'gui/src/components/provider-workspace/ProviderUsage.tsx' 'quotaRefresh|dash\.suppliers|shell\.|sys\.|vk\.|mod\.|fallback|type TKey|Record<TKey|useT' .
printf '%s\n' '--- en.ts relevant structure ---'
wc -l gui/src/i18n/en.ts
ast-grep outline gui/src/i18n/en.ts
sed -n '820,900p' gui/src/i18n/en.ts
printf '%s\n' '--- ProviderUsage relevant lines ---'
cat -n gui/src/components/provider-workspace/ProviderUsage.tsx | sed -n '120,175p'

Repository: lidge-jun/opencodex

Length of output: 31252


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

root = Path("gui/src/i18n")
key_re = re.compile(r'^\s*"([^"]+)"\s*:')
def keys(path):
    return {m.group(1) for line in path.read_text().splitlines() if (m := key_re.match(line))}

en = keys(root/"en.ts")
print(f"en.ts keys: {len(en)}")
for name in ["de.ts","ja.ts","ko.ts","nl.ts","ru.ts","zh.ts"]:
    ks = keys(root/name)
    print(f"{name}: explicit={len(ks)}, missing_from_en={len(en-ks)}, extra_not_in_en={len(ks-en)}")
    if ks-en:
        print("  extra:", ", ".join(sorted(ks-en)))

print("\nSelected en.ts keys:")
for key in [
    "prov.quotaRefresh", "prov.quotaRefreshing", "prov.quotaRefreshFailed",
    "prov.quotaRefreshAria", "dash.suppliers", "dash.refresh", "dash.refreshAll",
    "dash.fresh", "dash.noContactSince", "dash.modelsCount", "dash.showModels",
    "dash.hideModels", "dash.settingsSection", "dash.emptyKitchen",
    "dash.providerDisabled", "shell.navTraffic", "sys.manage", "vk.subtitle",
    "mod.subtitle",
]:
    print(key, key in en)
PY
printf '%s\n' '--- translation implementation and type declarations ---'
cat -n gui/src/i18n/shared.ts | sed -n '1,100p'
cat -n gui/src/i18n/en.ts | sed -n '1385,1410p'
printf '%s\n' '--- locale headers and composition ---'
for f in gui/src/i18n/de.ts gui/src/i18n/ja.ts gui/src/i18n/ru.ts gui/src/i18n/nl.ts; do
  echo "--- $f"
  sed -n '1,18p' "$f"
  tail -n 12 "$f"
done

Repository: lidge-jun/opencodex

Length of output: 12920


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

root = Path("gui/src")
# Capture literal keys passed directly to t(...), including multiline calls.
call_re = re.compile(r'\bt\s*\(\s*["\']([^"\']+)["\']')
keys = {}
for path in root.rglob("*"):
    if path.suffix not in {".ts", ".tsx"} or "/i18n/" in str(path):
        continue
    text = path.read_text()
    for m in call_re.finditer(text):
        keys.setdefault(m.group(1), []).append(f"{path}:{text.count(chr(10), 0, m.start()) + 1}")

en_text = Path("gui/src/i18n/en.ts").read_text()
en_keys = set(re.findall(r'^\s*"([^"]+)"\s*:', en_text, re.M))
missing = {key: locations for key, locations in keys.items() if key not in en_keys}
print(f"literal t() keys: {len(keys)}")
print(f"literal t() keys absent from en.ts: {len(missing)}")
for key in sorted(missing):
    print(f"{key}: {', '.join(missing[key])}")

print("\n--- shared translation provider implementation ---")
shared = Path("gui/src/i18n/shared.ts").read_text().splitlines()
for i, line in enumerate(shared, 1):
    if i >= 64:
        print(f"{i:4}: {line}")
PY
printf '%s\n' '--- all affected key usages ---'
rg -n --glob 'gui/src/**/*.{ts,tsx}' \
  'dash\.(suppliers|refresh|refreshAll|fresh|noContactSince|modelsCount|showModels|hideModels|stamp|settingsSection|emptyKitchen|providerDisabled)|shell\.|sys\.|vk\.|mod\.|prov\.quotaRefresh' gui/src

Repository: lidge-jun/opencodex

Length of output: 22517


Synchronize the locale key set before merging.

TKey is keyof typeof en (gui/src/i18n/en.ts:1401), and DICTS requires Record<TKey, string> (gui/src/i18n/shared.ts:13). Fifty-three literal t() call sites use keys absent from en.ts, including all four prov.quotaRefresh* keys in ProviderUsage.tsx:149-157. The dash.*, shell.*, sys.*, vk.*, and mod.* keys defined in ja.ts, ru.ts, and nl.ts are also absent from en.ts.

This produces invalid t() arguments and excess-property errors in ja.ts, ru.ts, and nl.ts. Add the complete 73-key set to en.ts, add matching values to the explicitly typed locale files, and regenerate or align the locale dictionaries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/i18n/en.ts` around lines 864 - 873, Synchronize the locale key set by
adding all 73 keys referenced by literal t() calls or defined in other locales
to the en export, including the prov.quotaRefresh* keys used by ProviderUsage
and the dash.*, shell.*, sys.*, vk.*, and mod.* groups. Add matching
translations or aligned fallback values to the explicitly typed ja, ru, and nl
dictionaries, then regenerate or reconcile locale dictionaries so TKey and DICTS
remain consistent without excess-property errors.

Source: Path instructions

Comment thread gui/src/i18n/ja.ts
Comment on lines +236 to +251
"dash.suppliers": "プロバイダー",
"dash.refresh": "更新",
"dash.refreshAll": "すべて更新",
"dash.retry": "再試行",
"dash.noContactSince": "{time} から接続がありません",
"dash.fresh": "最新 · {time}",
"dash.modelsCount": "{count} 個のモデル",
"dash.showModels": "モデルを表示",
"dash.hideModels": "モデルを隠す",
"dash.stamp.ready": "準備完了",
"dash.stamp.busy": "処理中",
"dash.stamp.idle": "待機中",
"dash.stamp.error": "エラー",
"dash.settingsSection": "設定",
"dash.emptyKitchen": "まだモデルがありません。",
"dash.providerDisabled": "無効",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Keys defined here do not exist in en.ts.

dash.suppliers, dash.refresh, dash.refreshAll, dash.fresh, dash.noContactSince, dash.modelsCount, dash.showModels, dash.hideModels, dash.stamp.*, dash.settingsSection, dash.emptyKitchen, dash.providerDisabled (lines 236-251), prov.quotaRefresh* (lines 306-309), and shell.*/sys.*/vk.*/mod.* (lines 1417-1471) are all absent from gui/src/i18n/en.ts.

Since locale files are typed Record<TKey, string> and TKey derives from en.ts, these extra properties are excess-property errors on the object literal, unless en.ts is updated to also define them.

See the consolidated comment on gui/src/i18n/en.ts#L864-L873 for the full cross-file list and the verification script.

Also applies to: 306-309, 1417-1471

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/i18n/ja.ts` around lines 236 - 251, Update the locale key definitions
in en.ts to include every key added in ja.ts, including dash.*,
prov.quotaRefresh*, shell.*, sys.*, vk.*, and mod.*; preserve the existing
English locale structure and values so the typed Record<TKey, string> accepts
ja.ts without excess-property errors.

Comment thread src/codex/routing.ts Outdated
Comment on lines +27 to +74

// Round-robin rotation cursor — persisted to ~/.opencodex/rotation-state.json so a
// process restart does not reset rotation back to account 0. Only consulted when
// config.codexRotationMode === "round-robin"; advances once per NEW (non-affined)
// conversation selection. Left untouched by the default failover path.
let rrCursor = loadPersistedRotationCursor();
let rrCursorDirty = false;

/** Reset the round-robin cursor. Intended for deterministic tests. */
export function resetCodexRoundRobinCursor(): void {
rrCursor = 0;
rrCursorDirty = true;
persistRotationCursor(rrCursor);
}

function rotationStatePath(): string {
const { join } = require("node:path") as typeof import("node:path");
const { getConfigDir } = require("../config") as typeof import("../config");
return join(getConfigDir(), "rotation-state.json");
}

function loadPersistedRotationCursor(): number {
try {
const { existsSync, readFileSync } = require("node:fs") as typeof import("node:fs");
const path = rotationStatePath();
if (!existsSync(path)) return 0;
const parsed = JSON.parse(readFileSync(path, "utf-8")) as { rrCursor?: unknown };
const n = Number(parsed.rrCursor);
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
} catch {
return 0;
}
}

/** Debounced write (writes immediately for now — cursor advances are rare, once per new conversation). */
function persistRotationCursor(value: number): void {
try {
const { writeFileSync, mkdirSync, chmodSync } = require("node:fs") as typeof import("node:fs");
const { getConfigDir } = require("../config") as typeof import("../config");
const dir = getConfigDir();
mkdirSync(dir, { recursive: true, mode: 0o700 });
const path = rotationStatePath();
writeFileSync(path, JSON.stringify({ rrCursor: value }), { mode: 0o600 });
try { chmodSync(path, 0o600); } catch { /* best-effort */ }
} catch {
/* persistence is best-effort */
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether src/config.ts (or its transitive imports) reference src/codex/routing.ts,
# which would explain the lazy require() pattern used for getConfigDir/node:fs/node:path.
rg -n "codex/routing" src/config.ts
rg -n "from \"\.\./config\"|require\(\"\.\./config\"\)" src/codex/routing.ts

Repository: lidge-jun/opencodex

Length of output: 385


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- routing.ts imports and persistence block ---'
sed -n '1,90p' src/codex/routing.ts

printf '%s\n' '--- config.ts imports and getConfigDir ---'
rg -n -C 4 'getConfigDir|codex/routing|from .*codex|require\(.*codex' src/config.ts src --glob '*.ts' --glob '!src/codex/routing.ts' | head -240

printf '%s\n' '--- Bun version pins and runtime configuration ---'
rg -n -i 'bun(@| version|Version|VERSION)|oven-sh/setup-bun|packageManager|engines' \
  package.json bun.lockb bun.lock .github Dockerfile* 2>/dev/null || true

Repository: lidge-jun/opencodex

Length of output: 19042


🌐 Web query:

Bun mkdirSync recursive existing directory EEXIST Windows bug fixed version

💡 Result:

The issue where Bun's mkdirSync(path, { recursive: true }) throws an EEXIST error on Windows when the directory already exists has been the subject of several iterative fixes and regressions in the Bun runtime [1][2][3][4][5]. There is no single "fixed" version that resolved all occurrences of this bug because it has resurfaced due to different root causes, including race conditions during concurrent file operations, handling of read-only attributes, and specific issues with relative paths containing .. [3][4][5]. Recent fixes were committed in April 2026, including: - Fixes for Windows-specific regressions involving mkdir race conditions with rm [4]. - Fixes for mkdir idempotency issues when using relative paths (e.g., ../dir) on Windows [5]. If you are experiencing this error, it is recommended to upgrade to the latest version of Bun available (check your current version via bun --version) to obtain these recent patches [4][5]. If you must support older versions or are still encountering issues, the most reliable workaround is to guard the directory creation with an existence check, as fs.existsSync(path) remains accurate [1][6]: import { mkdirSync, existsSync } from "node:fs"; if (!existsSync(targetDir)) { mkdirSync(targetDir, { recursive: true }); } This approach is safe and provides the expected idempotent behavior across all runtime versions [1][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- config.ts module header ---'
sed -n '1,80p' src/config.ts

printf '%s\n' '--- package runtime declarations ---'
sed -n '1,55p' package.json
sed -n '55,85p' bun.lock

printf '%s\n' '--- imports of routing.ts and config.ts ---'
rg -n '(^|from |import\(|require\().*(codex/routing|/config|"\.\./config|'\''\.\./config)' src --glob '*.ts' | head -300

printf '%s\n' '--- persistence call sites ---'
rg -n -C 3 'persistRotationCursor|rrCursorDirty|resetCodexRoundRobinCursor' src/codex/routing.ts

Repository: lidge-jun/opencodex

Length of output: 27391


🌐 Web query:

site:github.com/oven-sh/bun/issues mkdirSync recursive Windows EEXIST Bun 1.3.14

💡 Result:

In Bun 1.3.14, using mkdirSync(path, { recursive: true }) on Windows may throw an EEXIST error if the target directory already exists and has the ReadOnly attribute [1]. While recursive: true is intended to be idempotent—meaning it should return undefined without throwing an error if the directory already exists—this behavior has encountered regressions and environment-specific issues on Windows in various Bun versions [2][1][3]. Specifically for Bun 1.3.14: - The EEXIST error is triggered on Windows when the existing directory has the ReadOnly attribute [1]. This commonly affects directories synced by services like OneDrive [1]. - This issue is distinct from general recursive: true regressions seen in earlier Bun releases [4][3][5]. Recommended Workaround: If you are encountering this issue, you can guard the call by checking for the directory's existence first, which avoids triggering the problematic mkdirSync call on an existing directory [2][6]: import { existsSync } from "node:fs"; import { mkdirSync } from "node:fs"; if (!existsSync(targetPath)) { mkdirSync(targetPath, { recursive: true }); } This pattern is a safe, idempotent way to handle directory creation until the specific Windows attribute handling is fully resolved in the runtime [2].

Citations:


🌐 Web query:

site:github.com/oven-sh/bun/releases Bun 1.3.14 Windows mkdir recursive EEXIST

💡 Result:

The EEXIST error (Error: EEXIST, file already exists) when using recursive directory creation (e.g., fs.mkdirSync(path, { recursive: true })) in Bun on Windows typically occurs when the operation encounters a conflict or an invalid path state. While Bun v1.3.14 remains fully compatible with Node.js filesystem APIs, this specific error is often related to how the underlying operating system and Bun's filesystem abstraction handle directory creation attempts [1][2]. If you are encountering this issue, consider the following troubleshooting steps: 1. Path Conflicts: Verify that a file with the same name as one of the directories you are attempting to create does not already exist. Windows is case-insensitive, so conflicts can occur even with differing case spellings. 2. Permission and Locking Issues: Ensure that the process has appropriate write permissions for the target directory and that no other processes (such as antivirus scanners or backup software) have locked the path. 3. Node.js Compatibility: Since Bun strives for Node.js compatibility, ensure that the path structure is valid for Windows. In some versions of Bun, complex or malformed paths have historically caused issues during filesystem operations [3][4]. 4. Update/Verification: As of August 2026, Bun v1.3.14 is the latest stable release [1]. If the issue persists, check if it is reproducible in a standard Node.js environment to isolate whether it is a Bun-specific implementation detail or a broader environment issue. If the problem is unique to Bun, it is recommended to search the official Bun GitHub repository for open issues matching "mkdir recursive EEXIST" or to open a new issue with a minimal reproduction case if one does not exist [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- Bun issue metadata and relevant reports ---'
for issue in 34413 29521 16474 16466 16536 19090; do
  curl -fsSL "https://api.github.com/repos/oven-sh/bun/issues/$issue" |
    jq -r '"#\(.number) \(.state) \(.title)\nURL: \(.html_url)\n\(.body // "" | .[0:1800])\n---"'
done

printf '%s\n' '--- Bun 1.3.14 release date ---'
curl -fsSL https://api.github.com/repos/oven-sh/bun/releases/tags/bun-v1.3.14 |
  jq -r '"\(.tag_name) \(.published_at)\n\(.body // "" | .[0:1800])"'

Repository: lidge-jun/opencodex

Length of output: 580


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

ROOT = Path("src")
start = ROOT / "config.ts"
seen = set()
stack = [start]
edges = []

patterns = [
    re.compile(r'\bfrom\s+["\']([^"\']+)["\']'),
    re.compile(r'\bimport\s*\(\s*["\']([^"\']+)["\']\s*\)'),
    re.compile(r'\brequire\s*\(\s*["\']([^"\']+)["\']\s*\)'),
]

def resolve(src, spec):
    if not spec.startswith("."):
        return None
    base = (src.parent / spec).resolve()
    candidates = [base, *[Path(str(base) + ext) for ext in (".ts", ".tsx", ".js", ".mjs")],
                  base / "index.ts", base / "index.tsx"]
    for p in candidates:
        if p.is_file() and ROOT.resolve() in p.resolve().parents:
            return p
    return None

while stack:
    src = stack.pop()
    if src in seen:
        continue
    seen.add(src)
    text = src.read_text(errors="replace")
    for pattern in patterns:
        for spec in pattern.findall(text):
            dst = resolve(src, spec)
            if dst:
                edges.append((src, dst, spec))
                if dst not in seen:
                    stack.append(dst)

routing = (ROOT / "codex" / "routing.ts").resolve()
print(f"config reachable files: {len(seen)}")
print(f"routing reachable from config: {routing in seen}")
for src, dst, spec in edges:
    if dst == routing or src == start:
        print(f"{src} -> {dst} ({spec})")
PY

Repository: lidge-jun/opencodex

Length of output: 1055


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- all local require() usage in src ---'
rg -n '\brequire\s*\(' src --glob '*.ts' | head -240

Repository: lidge-jun/opencodex

Length of output: 738


🌐 Web query:

Bun GitHub issue 34413 mkdirSync recursive ReadOnly Windows Bun 1.3.14

💡 Result:

GitHub issue 34413 concerns an EEXIST error thrown by mkdirSync(path, { recursive: true }) on Windows when the target directory already exists and possesses the ReadOnly filesystem attribute [1]. This behavior deviates from Node.js, which expects recursive: true to handle existing directories idempotently without throwing errors [2]. The issue stems from how Bun's internal Windows filesystem operations identify existing directories [3]. Specifically, Bun's internal directoryExistsAt function can misclassify directories that have both the DIRECTORY and READONLY attributes as "not a directory," causing the mkdirSync operation to fail when it attempts to create a directory that is already present [3]. This is frequently encountered in corporate Windows environments (via Group Policy) or OneDrive-synced folders, where directories are automatically assigned the ReadOnly attribute [4][5][6]. As a common workaround, developers often catch and suppress the EEXIST error when it occurs during a recursive mkdirSync call [3][6]: javascript try { fs.mkdirSync(path, { recursive: true }); } catch (e: any) { if (e?.code !== "EEXIST") throw e; } This bug is part of a broader set of challenges Bun has faced with Windows filesystem path handling and attribute identification [2][7]. While Bun has made significant progress in matching Node.js filesystem behavior on Windows—including fixes for recursive idempotency involving relative paths [7] and directory access masks [8]—the specific interaction between recursive: true and the ReadOnly attribute remains a known point of friction [1][4][6].

Citations:


Handle Windows EEXIST and use static imports.

Bun 1.3.14 can throw EEXIST for mkdirSync(dir, { recursive: true }) when an existing Windows directory has the ReadOnly attribute. Handle EEXIST without skipping writeFileSync, or guard directory creation with existsSync.

No configuration import cycle reaches src/codex/routing.ts. Replace the require() calls at lines 43–65 with static node:fs, node:path, and getConfigDir imports, or document a concrete runtime reason for keeping them lazy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/codex/routing.ts` around lines 27 - 74, Update persistRotationCursor and
rotationStatePath/loadPersistedRotationCursor to use static imports for node:fs,
node:path, and getConfigDir instead of require calls. Ensure directory creation
tolerates Windows EEXIST for an existing read-only directory without skipping
writeFileSync, either by handling that error or checking existence before
mkdirSync; preserve the existing best-effort persistence behavior.

Comment thread src/codex/routing.ts Outdated
Comment on lines 710 to 725
// Opt-in round-robin: rotate NEW (non-affined) conversations across the usable
// pool. getEligiblePoolAccounts already filters reauth / cooldown / soft-avoid /
// unusable accounts and returns a deterministic order (main unshifted first,
// then config order), so the cursor yields a stable rotation. activeCodexAccountId
// is intentionally left untouched (no setActiveCodexAccount / saveConfig). With a
// single usable account this collapses to that account (no-op). Runs before the
// sticky failover path so thread affinity established elsewhere is preserved.
if (config.codexRotationMode === "round-robin") {
const pool = getEligiblePoolAccounts(config);
if (pool.length) {
const pick = pool[rrCursor % pool.length]!;
rrCursor = (rrCursor + 1) % Math.max(pool.length, 1);
persistRotationCursor(rrCursor);
return { status: "selected", accountId: pick };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix: pass now through to getEligiblePoolAccounts in the round-robin branch.

getEligiblePoolAccounts(config, excludeId?, now = Date.now()) at line 490 defaults now when omitted. Line 718 calls getEligiblePoolAccounts(config) without a third argument, so this branch always uses a freshly-computed Date.now() instead of the now parameter already threaded through resolveCodexAccountForThreadDetailed. Every other filtering call in this function (isThreadAffinityExpired, shouldFailover, isCodexAccountSelectable, bindThreadAffinity) consistently uses the passed now.

Today the practical effect is negligible because the two Date.now() calls happen microseconds apart. But this breaks the time-injection contract this file otherwise maintains for deterministic testing, and it introduces a latent inconsistency: a caller that passes a fixed now (for example, time-travel tests of cooldown/soft-avoid boundaries) gets a round-robin pool computed against real wall-clock time instead of the intended simulated time.

🔧 Proposed fix
   if (config.codexRotationMode === "round-robin") {
-    const pool = getEligiblePoolAccounts(config);
+    const pool = getEligiblePoolAccounts(config, undefined, now);
     if (pool.length) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Opt-in round-robin: rotate NEW (non-affined) conversations across the usable
// pool. getEligiblePoolAccounts already filters reauth / cooldown / soft-avoid /
// unusable accounts and returns a deterministic order (main unshifted first,
// then config order), so the cursor yields a stable rotation. activeCodexAccountId
// is intentionally left untouched (no setActiveCodexAccount / saveConfig). With a
// single usable account this collapses to that account (no-op). Runs before the
// sticky failover path so thread affinity established elsewhere is preserved.
if (config.codexRotationMode === "round-robin") {
const pool = getEligiblePoolAccounts(config);
if (pool.length) {
const pick = pool[rrCursor % pool.length]!;
rrCursor = (rrCursor + 1) % Math.max(pool.length, 1);
persistRotationCursor(rrCursor);
return { status: "selected", accountId: pick };
}
}
// Opt-in round-robin: rotate NEW (non-affined) conversations across the usable
// pool. getEligiblePoolAccounts already filters reauth / cooldown / soft-avoid /
// unusable accounts and returns a deterministic order (main unshifted first,
// then config order), so the cursor yields a stable rotation. activeCodexAccountId
// is intentionally left untouched (no setActiveCodexAccount / saveConfig). With a
// single usable account this collapses to that account (no-op). Runs before the
// sticky failover path so thread affinity established elsewhere is preserved.
if (config.codexRotationMode === "round-robin") {
const pool = getEligiblePoolAccounts(config, undefined, now);
if (pool.length) {
const pick = pool[rrCursor % pool.length]!;
rrCursor = (rrCursor + 1) % Math.max(pool.length, 1);
persistRotationCursor(rrCursor);
return { status: "selected", accountId: pick };
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/codex/routing.ts` around lines 710 - 725, Update the round-robin branch
in resolveCodexAccountForThreadDetailed to pass its threaded now value as the
third argument to getEligiblePoolAccounts, preserving the injected-time behavior
used by the other account-selection checks.

Comment thread src/providers/fallback.ts
Comment on lines +134 to +167
export function providerFallbackPlan(
config: OcxConfig,
route: { provider: string; modelId: string },
): ProviderFallbackPlan | null {
// A physical provider literally named "combo" is only kept addressable while no combos
// exist (preservesPhysicalComboProvider). Injecting one here would silently shadow it.
if (Object.hasOwn(config.providers, COMBO_NAMESPACE)) return null;

const configured = providerFallbackTargets(config.providers[route.provider]);
if (configured.length === 0) return null;

const usable = (target: OcxComboTarget): boolean => {
const provider = config.providers[target.provider];
return !!provider && provider.disabled !== true;
};
if (!usable({ provider: route.provider, model: route.modelId })) return null;

const targets: OcxComboTarget[] = [{ provider: route.provider, model: route.modelId }];
const seen = new Set<string>([targetKey(targets[0]!)]);
for (const target of configured) {
const key = targetKey(target);
if (seen.has(key) || !usable(target)) continue;
seen.add(key);
targets.push(target);
}
if (targets.length < 2) return null;

const comboId = syntheticComboId(route.provider, route.modelId);
const combo: OcxComboConfig = { targets, strategy: "failover" };
return {
comboId,
config: { ...config, combos: { ...config.combos, [comboId]: combo } },
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Silently-inactive fallback configuration has no diagnostic trail.

providerFallbackIssues (Line 91-118) does not check whether a fallback target's provider is disabled. A fully valid-looking fallback list can therefore be saved even when every target is disabled. At request time, usable() (Line 145-149) filters out disabled providers, and if fewer than 2 usable targets remain, providerFallbackPlan returns null (Line 159) with no log line anywhere. An admin who configured fallback pointing only at currently-disabled providers gets no indication that it is silently inactive.

Add a one-line console.warn/debug log when providerFallbackPlan returns null because targets dropped to fewer than 2 due to usable() filtering (as opposed to the "no fallback configured" early return at Line 143), so this is diagnosable without reading source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/fallback.ts` around lines 134 - 167, Add a one-line
console.warn or debug log in providerFallbackPlan when usable() filtering leaves
fewer than two targets, immediately before the targets.length < 2 return.
Include the route provider/model and indicate that configured fallback targets
are disabled or unusable; preserve the existing silent return for no configured
fallback and other early-return cases.

Comment thread src/providers/quota.ts Outdated
Comment on lines +664 to +679
export async function fetchSingleProviderQuotaReport(
config: OcxConfig,
name: string,
): Promise<{ generatedAt: number; report: ProviderQuotaReport | null }> {
const provider = Object.hasOwn(config.providers, name) ? config.providers[name] : undefined;
if (!provider) return { generatedAt: Date.now(), report: null };
const key = cacheKey(config);
const epoch = invalidationEpoch;
const report = await maybeFetchProviderQuota(name, provider, config, true);
if (report && epoch === invalidationEpoch && cache && cache.key === key) {
const reports = cache.response.reports.filter(item => item.provider !== name);
reports.push(report);
cache = { key, ts: cache.ts, response: { generatedAt: Date.now(), reports } };
}
return { generatedAt: Date.now(), report };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a focused regression test for fetchSingleProviderQuotaReport.

This new export has non-trivial cache-merge semantics (skip merge when no matching cache exists, skip merge when invalidationEpoch moved, preserve cache.ts, replace only the probed provider's row). None of the reviewed test files exercise it directly. Add a Bun test near the other quota-related tests that covers: no pre-existing cache (must not create one), a matching cache (row replaced), and a stale/invalidated epoch (merge skipped).

Based on path instructions for tests/**: "A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/quota.ts` around lines 664 - 679, Add a focused Bun regression
test for the exported fetchSingleProviderQuotaReport, covering no-cache behavior
without creating cache, matching-cache behavior that replaces only the queried
provider while preserving cache.ts, and invalidationEpoch changes that skip the
merge. Place it alongside existing quota tests and use controlled provider/cache
fixtures to assert each outcome.

Source: Path instructions


🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find every caller of the two quota-fetch entry points to assess how often they can run concurrently.
rg -n "fetchSingleProviderQuotaReport|fetchProviderQuotaReports" -g '*.ts' -g '*.tsx' src gui 2>/dev/null

Repository: lidge-jun/opencodex

Length of output: 1717


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- quota outline ---'
ast-grep outline src/providers/quota.ts --match 'fetchSingleProviderQuotaReport' --view expanded
ast-grep outline src/providers/quota.ts --match 'fetchProviderQuotaReports' --view expanded
printf '%s\n' '--- quota implementation ---'
sed -n '600,740p' src/providers/quota.ts
printf '%s\n' '--- management API caller ---'
sed -n '1,110p' src/server/management-api.ts
printf '%s\n' '--- provider route caller ---'
sed -n '1,110p' src/server/management/provider-routes.ts
printf '%s\n' '--- all exact references ---'
rg -n -C 3 'fetchSingleProviderQuotaReport|fetchProviderQuotaReports' src tests 2>/dev/null
printf '%s\n' '--- quota-related files ---'
git ls-files | rg '(^|/)(quota|.*quota.*|.*provider.*test.*|.*management.*test.*)' | head -100

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- quota implementation ---'
sed -n '600,740p' src/providers/quota.ts
printf '%s\n' '--- all exact references ---'
rg -n -C 3 'fetchSingleProviderQuotaReport|fetchProviderQuotaReports' src tests 2>/dev/null
printf '%s\n' '--- quota-related tests ---'
git ls-files | rg '(^|/)(quota|.*quota.*|.*provider.*test.*|.*management.*test.*)'

Repository: lidge-jun/opencodex

Length of output: 39359


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- GUI quota hook ---'
sed -n '1,240p' gui/src/use-provider-quotas.ts
printf '%s\n' '--- GUI quota and refresh references ---'
rg -n -C 4 'provider-quotas|quota.*refresh|refresh.*quota|fetchSingleProviderQuotaReport' gui src tests --glob '*.{ts,tsx,js,jsx}' | head -300
printf '%s\n' '--- route and export references across all tracked text files ---'
rg -n -C 2 'fetchSingleProviderQuotaReport|/api/provider-quotas' . --glob '!devlog/**' --glob '!*.svg' | head -300
printf '%s\n' '--- test declaration range ---'
sed -n '630,755p' tests/provider-quota.test.ts

Repository: lidge-jun/opencodex

Length of output: 37743


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- hook consumers ---'
rg -n -C 5 'useProviderQuotas|refreshProvider|refreshAll' gui/src gui/tests --glob '*.{ts,tsx}'
printf '%s\n' '--- backend route exact logic ---'
sed -n '55,78p' src/server/management/provider-routes.ts
printf '%s\n' '--- route tests for provider query ---'
rg -n -C 5 'provider-quotas.*provider|provider.*provider-quotas|refreshProvider' tests gui/tests --glob '*.{ts,tsx}'
printf '%s\n' '--- deterministic cache ordering model ---'
python3 - <<'PY'
cache = {
    "key": "same",
    "reports": [
        {"provider": "target", "value": "old"},
        {"provider": "other", "value": "old"},
    ],
}
previous = [dict(row) for row in cache["reports"]]

cache["reports"] = [row for row in cache["reports"] if row["provider"] != "target"]
cache["reports"].append({"provider": "target", "value": "single-fresh"})
single_snapshot = [dict(row) for row in cache["reports"]]

by_provider = {row["provider"]: dict(row) for row in previous}
for row in [{"provider": "target", "value": "full-fresh"}]:
    by_provider[row["provider"]] = dict(row)
cache["reports"] = list(by_provider.values())

print("after single merge:", single_snapshot)
print("after later full commit:", cache["reports"])
print("single row retained:", any(row == {"provider": "target", "value": "single-fresh"} for row in cache["reports"]))
PY

Repository: lidge-jun/opencodex

Length of output: 7610


🏁 Script executed:

#!/bin/bash
set -e
rg -n -C 5 'useProviderQuotas|refreshProvider|refreshAll' gui/src gui/tests --glob '*.{ts,tsx}'
sed -n '55,78p' src/server/management/provider-routes.ts
rg -n -C 5 'provider-quotas.*provider|provider.*provider-quotas|refreshProvider' tests gui/tests --glob '*.{ts,tsx}'

Repository: lidge-jun/opencodex

Length of output: 7514


🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
def route_dispatch(query):
    # Mirrors the current route's implemented decision: it reads only `refresh`.
    force_refresh = query.get("refresh") in {"1", "true"}
    return ("fetchProviderQuotaReports", force_refresh)

print("aggregate request:", route_dispatch({}))
print("forced aggregate request:", route_dispatch({"refresh": "1"}))
print("provider-scoped request:", route_dispatch({"provider": "cursor"}))

previous = [
    {"provider": "target", "value": "old"},
    {"provider": "other", "value": "old"},
]
cache = [dict(row) for row in previous]

# Single-provider merge completes first.
cache = [row for row in cache if row["provider"] != "target"]
cache.append({"provider": "target", "value": "single-fresh"})
print("after single-provider merge:", cache)

# Older full-refresh operation commits from its `previous` snapshot.
by_provider = {row["provider"]: dict(row) for row in previous}
for row in [{"provider": "target", "value": "full-fresh"}]:
    by_provider[row["provider"]] = dict(row)
cache = list(by_provider.values())
print("after later full-refresh commit:", cache)
print("single-provider row survived:", any(row["value"] == "single-fresh" for row in cache))
PY

Repository: lidge-jun/opencodex

Length of output: 611


Implement the per-provider quota route. gui/src/use-provider-quotas.ts:103-105 sends ?provider=<name>, but src/server/management/provider-routes.ts:67-69 ignores it and always calls fetchProviderQuotaReports. fetchSingleProviderQuotaReport is unused, and its { report } response also does not match the hook's expected { reports } shape. Dispatch provider-scoped requests to the single-provider function, adapt the response shape, wire the hook, and add focused tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/quota.ts` around lines 664 - 679, Implement provider-scoped
quota requests in the provider management route by detecting the provider query
parameter and calling fetchSingleProviderQuotaReport instead of
fetchProviderQuotaReports. Adapt its single report result into the hook’s
expected reports response shape, preserving the existing aggregate behavior when
no provider is supplied. Update use-provider-quotas to use the scoped response
correctly and add focused tests covering both request paths and response shapes.

Comment on lines 638 to +647
const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
? (rawBody as { model: string }).model
: `combo/${comboId}`;
Object.assign(logCtx, {
requestedModel,
model: requestedModel,
provider: "combo",
comboId,
});
// A per-provider fallback chain runs on this same hop loop but is not a combo the user
// configured: the log row must keep the winning target's own provider/model rather than
// collapsing into a synthetic `combo` row nothing in the GUI can open.
const comboIdentity = isProviderFallbackComboId(comboId)
? { requestedModel }
: { requestedModel, model: requestedModel, provider: "combo", comboId };
Object.assign(logCtx, comboIdentity);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Sanitize the requestedModel fallback default too — it can still embed the synthetic combo's NUL byte.

This diff sanitizes comboId for display at Line 650 (Unknown combo: ${comboIdLabel(comboId)}), Line 689 (comboUnavailableResponse), and Line 835 (console.warn) — all needed because syntheticComboId (src/providers/fallback.ts, Line 33-35) embeds a literal \u0000 byte for provider-fallback combos, and comboIdLabel exists specifically to turn that into a readable fallback:<provider>/<model> string.

Line 638-640 was not updated:

const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
  ? (rawBody as { model: string }).model
  : `combo/${comboId}`;

Before this PR, comboId here always came from a user-configured combo name (no NUL possible). Now that providerFallbackPlan (Line 950-959) can route a plain request into handleComboResponses with a NUL-embedding synthetic comboId, this fallback branch can propagate a raw NUL byte into logCtx.requestedModel whenever the incoming body lacks a string model field. That value flows into RequestLogContext.requestedModel, which is persisted/serialized for request logs — a control character landing in persisted log data is exactly the failure this diff's other 3 fixes were meant to prevent.

🐛 Proposed fix
   const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
     ? (rawBody as { model: string }).model
-    : `combo/${comboId}`;
+    : `combo/${comboIdLabel(comboId)}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
? (rawBody as { model: string }).model
: `combo/${comboId}`;
Object.assign(logCtx, {
requestedModel,
model: requestedModel,
provider: "combo",
comboId,
});
// A per-provider fallback chain runs on this same hop loop but is not a combo the user
// configured: the log row must keep the winning target's own provider/model rather than
// collapsing into a synthetic `combo` row nothing in the GUI can open.
const comboIdentity = isProviderFallbackComboId(comboId)
? { requestedModel }
: { requestedModel, model: requestedModel, provider: "combo", comboId };
Object.assign(logCtx, comboIdentity);
const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
? (rawBody as { model: string }).model
: `combo/${comboIdLabel(comboId)}`;
// A per-provider fallback chain runs on this same hop loop but is not a combo the user
// configured: the log row must keep the winning target's own provider/model rather than
// collapsing into a synthetic `combo` row nothing in the GUI can open.
const comboIdentity = isProviderFallbackComboId(comboId)
? { requestedModel }
: { requestedModel, model: requestedModel, provider: "combo", comboId };
Object.assign(logCtx, comboIdentity);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/responses/core.ts` around lines 638 - 647, Sanitize the fallback
assigned to requestedModel in handleComboResponses: when the request body lacks
a string model, use comboIdLabel(comboId) instead of interpolating the raw
comboId. Preserve the incoming string model unchanged and keep the existing
comboIdentity behavior intact.

OnlineChef and others added 2 commits August 1, 2026 07:20
… fallback hops

- keep the Anthropic terminal-continuation guard active on the primary hop of a
  per-provider fallback chain (synthetic combos are not user combos)
- hand the pre-expansion body to the fallback combo so previous_response_id is
  expanded exactly once
- sweep expired combo-target cooldowns so synthetic combo ids cannot accumulate
  state forever
- resolve the initial GUI route from the hash through resolveAppHashChange so a
  bookmarked #codex-auth lands on Providers
- cover the fallback editor interaction (ordered patch, incomplete row blocks save)

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
React Doctor's no-array-index-as-key fired on the fallback editor: rows are
user-reorderable and removable, so the index is not a stable identity. Carry a
per-row id in form state and drive updates/removal off it.

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
@OnlineChef OnlineChef closed this Aug 1, 2026
@OnlineChef
OnlineChef deleted the feat/all-provider-account-management branch August 1, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant