Verified on cita1 with a single temporary Metapi instance: health 200, /api/site-announcements 200, Playwright page reload rendered announcements without Failed query, and browser console had no errors. Production container was restored and remains healthy.
* feat: add context_length to /v1/models response
- New modelContextLengthCache service for in-memory model→context_length mapping
- Platform adapters (newApi, standardApiProvider) extract context_length from upstream /v1/models
- modelsSurface injects context_length into both OpenAI and Claude response formats
- Default 1,000,000 when upstream does not provide context_length
- Supports field names: context_length, contextLength, max_context_length, contextWindow, etc.
* feat: add delete button for manually added models
- Backend: DELETE /api/accounts/:id/models/manual endpoint
- Only deletes models where isManual=true (safe against auto-discovered models)
- Frontend: AccountModelsModal shows '✕ 删除' button next to each manual model
- Frontend: api.removeAccountManualModels() function
- Accounts.tsx: wires up delete handler with toast feedback
* fix: address PR review feedback for model metadata
* fix: preserve model context metadata across credential scans
* fix: canonicalize endpoint model context scope
Closes#507
* fix: isolate concurrent model context scan scopes
* fix: use conservative model context lengths in models surface
* fix: clear stale context metadata after failed refresh
* fix: keep model context metadata conservative
---------
Co-authored-by: Hermes Agent <hermes-agent@metapi>
Co-authored-by: Codex <codex@local>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: cita-777 <juricek.chen@gmail.com>
* feat: add OrcaRouter as a named platform and provider preset
Add a dedicated OrcaRouter adapter (OpenAI-compatible, API-key-first),
auto-detected for api.orcarouter.ai, and registered across the platform
registry, URL detection, site initialization presets, brand matching,
and the web UI (site options, payload rules, connection segments).
Verified: 2581 tests passing, full typecheck green, and a live model
discovery call against https://api.orcarouter.ai returns 200 models.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: JinhaoSong322 <jinhao.song@myflashcloud.com>
* fix: bound OrcaRouter host detection
---------
Signed-off-by: JinhaoSong322 <jinhao.song@myflashcloud.com>
Co-authored-by: JinhaoSong322 <jinhao.song@myflashcloud.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: cita-777 <juricek.chen@gmail.com>
* fix: use the user id returned by login instead of guessing it
New API's /api/user/login returns the account's own id in `data.id`, but
`login()` extracted only the access token and threw the rest away. Callers
therefore fell back to `guessPlatformUserIdFromUsername()`, whose trailing
digits regex only matches usernames that happen to end with the id (e.g.
`linuxdo_80305`). For an email login such as `alice@example.com` it yields
nothing, so requests go out without the `New-Api-User` header, the site
answers 401, the balance stays 0 and the session is misjudged as expired —
which sends every scheduled round back through a full password login.
Surface `platformUserId` on LoginResult, populate it from the login payload,
prefer it over the guess when creating an account, and persist it on
auto-relogin too (merging extraConfig so the stored autoRelogin credentials
survive). This also avoids the extra `discoverUserId()` round trip that
checkin and balance refresh currently pay on every single call.
* fix: carry the re-login identity back to the caller
Addresses the review feedback on #610.
`tryAutoRelogin()` persisted the authoritative `platformUserId` but returned
only the access token, so callers kept using pre-login state:
- the immediate retry still sent the stale `New-Api-User`, so it could fail
even though the re-login succeeded
- the later `mergeAccountExtraConfig(account.extraConfig, ...)` writes started
from the pre-login snapshot and wrote the old id back over the one that had
just been persisted
Both helpers now return `{ accessToken, platformUserId, extraConfig }`.
`checkinService` adopts the id and refreshes `account.extraConfig` before
retrying; `balanceService` does the same via its existing `activeExtraConfig`
(`readBalance` closes over `platformUserId`, so the retry picks it up).
Also from the review:
- `extractLoginUserId()` no longer uses `Number.parseInt`, which accepted
"80305abc" and "80305.9" as 80305 and would have produced a wrong
`New-Api-User`. It now requires an all-digit string and a safe integer.
- `login()` returns the exported `LoginResult` instead of redeclaring the shape
inline, so the base contract and the adapter cannot drift apart.
* fix: never let the guessed id overwrite the one login reported
Follow-up to the review on #610.
`storedPlatformUserId` and `guessedPlatformUserId` are computed once, before any
re-login. When an account had no stored id, the guess was non-empty, and a
re-login then reported the authoritative id, the success path still tested the
pre-login snapshot and merged the guess back in — replacing a known-good id with
digits scraped off the end of the username, which is the exact failure this PR
exists to remove.
The condition now checks the *current* `account.extraConfig` instead of the
pre-login snapshot. `guessedPlatformUserId` is only ever set when
`storedPlatformUserId` was empty, so behaviour is unchanged when no re-login
happened.
* test: cover the relogin identity path
Per the review, the existing auto-relogin test only asserted the refreshed access
token, so nothing pinned down the behaviour this PR is about.
The new case uses `alice_1999` — a username whose trailing digits are not the
account id, which is exactly what `guessPlatformUserIdFromUsername()` gets wrong
— and asserts three things:
- the first attempt falls back to the guess (1999), since nothing better exists yet
- the retry after re-login uses the id the site reported (500123)
- no subsequent write puts the guess back over it
Verified the test actually bites: reverting either the identity propagation or the
guarded guess-persist makes it fail.
* test: also pin that relogin keeps the autoRelogin credentials
Per the review, asserting only `platformUserId` left a hole: a write that
*replaced* `extraConfig` with just `{ platformUserId }` would still satisfy the
test, while dropping the `autoRelogin` credentials and leaving nothing to
re-login with on the next round.
The assertion now parses the whole persisted config and requires both the
reported id and the intact credentials. Verified it bites: swapping
`mergeAccountExtraConfig` for a plain overwrite makes it fail.
* fix: resolve the login user id for every adapter, not just New API
`VeloeraAdapter` inherits `BasePlatformAdapter.login()` untouched, and that base
implementation discarded `res.data.id` exactly like the New API one did. Its
`authHeaders()` only sends `Veloera-User` / `New-API-User` / `User-id` when an id
was resolved, so the same e-mail-login failure this PR fixes for New API was
still present on the adjacent adapter — and on anything else inheriting the base
login path.
Moved `extractLoginUserId()` from `NewApiAdapter` onto `BasePlatformAdapter` as a
protected helper and used it in the base `login()`. `NewApiAdapter` now inherits
it instead of carrying a private copy, so the diff gets smaller while covering
more adapters.
Also tightened the test assertion: credentials are now required to survive
*every* persisted config, not just one of them. `toContainEqual` would have
passed while a later write dropped them.
* refactor: keep login identity resolution out of route
* fix: preserve concurrent config during relogin
* fix: avoid replaying relogin config snapshots
---------
Co-authored-by: Jacky Li <xiaoranli@buct.edu.cn>
Co-authored-by: cita-777 <juricek.chen@gmail.com>
* feat(chart): support referencing a pre-existing Secret via existingSecret
Add an existingSecret value to the k3s chart so operators can supply a
cluster-managed Secret instead of letting the chart generate one.
- existingSecret non-empty: skip rendering secret.yaml; Deployment envFrom
references the named Secret; checksum/env-secret annotation omitted (the
chart does not promise to roll on external Secret content changes).
- existingSecret empty: behavior unchanged (chart creates <release>-env and
references it; full backward compatibility).
- When existingSecret is set, .Values.env.* required validations are skipped
via the conditional render (no conflict detection with env.*).
- No per-key mapping (still envFrom), no lookup/verification of the external
Secret, no new NOTES.txt/README — the chart's documentation surface is left
as-is.
Verified with helm template in both modes.
Closes#595
Co-Authored-By: Claude <noreply@anthropic.com>
* test(chart): assert envFrom resolves to envSecretRefName for existingSecret
The k3sAssets guard asserted envFrom referenced metapi.envSecretName, but the
existingSecret feature renamed the envFrom binding to metapi.envSecretRefName
(secretRef.name falls back to the helper when existingSecret is unset). Update
the assertion to match and pin the new existingSecret branch with guards on the
envSecretRefName define and the .Values.existingSecret branch in _helpers.tpl.
Co-Authored-By: Claude <noreply@anthropic.com>
* docs(k3s): document existingSecret mode, required keys, and roll caveat
Add a dedicated section to the k3s update-center doc covering the chart's
existingSecret value: the chart-managed Secret vs an operator-supplied one, the
fixed 11 keys the external Secret must provide, and the behavioral change that
the checksum/env-secret auto-roll on Secret change is omitted under
existingSecret (operators must drive rollouts via Reloader or manual restart).
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(docker): forward every documented env var to the container
docker/docker-compose.yml only forwarded a subset of the variables that
docker/.env.example tells users to set, so the rest were silently inert.
CHECKIN_CRON and BALANCE_REFRESH_CRON were additionally hardcoded, which
overrode any user value.
* docs(docker): declare the remaining supported env vars in the compose example
* docs: add missing Telegram env vars to .env.example
TELEGRAM_API_BASE_URL, TELEGRAM_MESSAGE_THREAD_ID, and
TELEGRAM_USE_SYSTEM_PROXY were supported in config.ts but absent from
the example file.
* fix(config): read TELEGRAM_API_BASE_URL from environment
telegramApiBaseUrl was hardcoded to 'https://api.telegram.org' with no
env var override, preventing users in restricted networks (mainland
China, self-hosted bot-api servers) from pointing metapi at a proxy.
Now reads TELEGRAM_API_BASE_URL with fallback to the official endpoint,
trims whitespace, and strips trailing slashes — matching the pattern
already applied in notifyService.ts line ~80.
* fix: make telegramApiBaseUrl configurable via TELEGRAM_API_BASE_URL env var
Hardcoding https://api.telegram.org prevents users behind the Great Firewall
or running a self-hosted Bot API server from configuring a proxy base URL.
Read the base URL from TELEGRAM_API_BASE_URL (fallback to official endpoint)
and strip trailing slashes.
* test: cover TELEGRAM_API_BASE_URL resolution in buildConfig
* style: drop stray trailing newline
* fix(config): normalize TELEGRAM_API_BASE_URL before applying the default
* test: cover whitespace-only TELEGRAM_API_BASE_URL fallback
The old star history chart depends on the GitHub stargazer API which broke recently. Switch image and anchor URLs in README.md and README_EN.md to the self-hosted star-history.dera.page endpoint (same-domain, no api subdomain, /svg path kept, hash-based links).
Co-authored-by: OctoBored <212877535+OctoBored@users.noreply.github.com>
The workflow stack should use the current checkout action now that the full CI matrix is green, keeping automation dependencies maintained without changing runtime behavior.
Constraint: This is a GitHub Actions dependency update generated by Dependabot.
Rejected: Holding the checkout major update without a failing signal | PR CI and CodeQL passed across the project gate.
Confidence: high
Scope-risk: narrow
Directive: Re-check pull_request_target behavior before future checkout major upgrades because v7 changes fork checkout protections.
Tested: GitHub CI for PR #587 passed Test Core, Build Web, Build Server, Build Desktop, Typecheck, schema checks for SQLite/MySQL/Postgres, Build Docs, CodeQL, and audit.
Not-tested: Docker publish jobs were skipped because this was a pull_request workflow, not a push/tag publish event.
New API-compatible deployments disagree on which user-id header session-cookie calls must send, so Metapi should emit the compatible header set wherever it has a resolved platform user id.
Constraint: Some New API deployments require X-User-Id while existing integrations already depend on New-Api-User variants.
Rejected: Replacing the existing header variants | it would regress deployments that already accept the current names.
Confidence: high
Scope-risk: narrow
Directive: Keep New API session-cookie auth header changes centralized in the shared helper.
Tested: GitHub CI for PR #567 passed Test Core, Build Web, Build Server, Build Desktop, Typecheck, schema checks for SQLite/MySQL/Postgres, CodeQL, and audit.
Not-tested: No fresh post-merge local runtime smoke test was run.
Allow Claude sites configured under an /anthropic path to fall back to the parent OpenAI-compatible models endpoint so model refresh works without changing chat routing.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Constraint: Dependabot replacement PR after earlier grouped dependency PR was superseded; all required GitHub checks passed before merge.
Confidence: high
Scope-risk: moderate
Directive: Treat this as the canonical replacement for superseded #544.
Tested: GitHub CI status check rollup passed for PR before merge.
Not-tested: Manual runtime smoke beyond repository CI.
Constraint: Dependabot PR with required GitHub checks passing before merge.
Confidence: high
Scope-risk: narrow
Tested: GitHub CI status check rollup passed for PR before merge.
Not-tested: No additional manual runtime smoke beyond repository CI.
Constraint: Dependabot PR with required GitHub checks passing before merge.
Confidence: high
Scope-risk: narrow
Tested: GitHub CI status check rollup passed for PR before merge.
Not-tested: No additional manual runtime smoke beyond repository CI.
Constraint: Dependabot PR with required GitHub checks passing before merge.
Confidence: high
Scope-risk: narrow
Tested: GitHub CI status check rollup passed for PR before merge.
Not-tested: No additional manual runtime smoke beyond repository CI.
Constraint: Dependabot PR with all required GitHub checks passing before merge.
Confidence: high
Scope-risk: narrow
Tested: GitHub CI status check rollup passed for PR before merge.
Not-tested: Combined post-merge dependency matrix beyond repository CI.
Constraint: Dependabot PR with all required GitHub checks passing before merge.
Confidence: high
Scope-risk: narrow
Tested: GitHub CI status check rollup passed for PR before merge.
Not-tested: Combined post-merge dependency matrix beyond repository CI.
Constraint: Dependabot PR with all required GitHub checks passing before merge.
Confidence: high
Scope-risk: narrow
Tested: GitHub CI status check rollup passed for PR before merge.
Not-tested: Combined post-merge dependency matrix beyond repository CI.
Constraint: main push CI fails on npm audit --omit=dev --audit-level=high after recent merges.
Rejected: Broad npm audit fix output | it updated unrelated dev-only lockfile entries.
Confidence: high
Scope-risk: narrow
Directive: Keep PR audit informational until production dependencies are audited on dev/main consistently.
Tested: ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm ci --prefer-offline --no-audit --no-fund; npm audit --omit=dev --audit-level=high; npm run typecheck:server; npm test -- --run src/server/config.test.ts src/server/settingsRoutePayloads.test.ts
Not-tested: Full GitHub Actions matrix before opening PR.
Merged after updating the PR onto current main and fixing the remaining probe-setting review gaps.\n\nValidation:\n- GitHub checks all passing: Test Core, Typecheck, Build Web/Server/Desktop, Repo Drift Check, Schema SQLite/MySQL/Postgres, CodeQL, CodeRabbit.\n- Local: npm run typecheck\n- Local: npm run repo:drift-check\n- Local: npx vitest run --root . src/server/db/schemaContract.test.ts src/server/db/schemaArtifactGenerator.test.ts src/server/db/schemaIntrospection.test.ts src/server/db/schemaParity.test.ts src/web/api.test.ts src/web/pages/helpers/sitesEditor.test.ts src/web/pages/sites.disabled-models-save.test.tsx src/web/pages/sites.edit-scroll.test.tsx
Refresh the PR onto current main and close the remaining probe-setting review gaps: the primary site save now persists the latency threshold, the site row type carries that field without an any-cast, active probe streams are aborted on unmount, and all-model one-shot probes get the longer timeout they need.
Constraint: PR #510 must preserve the per-site model probe feature while merging cleanly into current main.
Rejected: Leave threshold persistence behind the separate probe-settings button | the primary save path would still silently drop user changes.
Rejected: Keep the SiteRow any-cast | it hides API/schema drift for the new persisted field.
Confidence: high
Scope-risk: narrow
Directive: Keep probe setting fields represented in the typed site save payload and covered by focused web API/editor tests.
Tested: npm run typecheck; npm run repo:drift-check; npx vitest run --root . src/server/db/schemaContract.test.ts src/server/db/schemaArtifactGenerator.test.ts src/server/db/schemaIntrospection.test.ts src/server/db/schemaParity.test.ts src/web/api.test.ts src/web/pages/helpers/sitesEditor.test.ts src/web/pages/sites.disabled-models-save.test.tsx src/web/pages/sites.edit-scroll.test.tsx
Not-tested: Full npm test.
The release branch had fallen behind current main and only conflicted in the
Update Center recent-tag test. Keep the latest main-side deploy-state coverage
while preserving the PR's raw-tag filtering and digest visibility assertions.
Constraint: PR #495 must merge into the current main without widening the PR diff beyond the update-center candidate behavior.
Rejected: Drop the PR-specific digest/raw-tag assertions | would regress the review fixes that prompted the branch update.
Confidence: high
Scope-risk: narrow
Directive: Keep recent Docker tag cards deployable only from real tagName values and keep digest metadata visible before deployment.
Tested: npx vitest run --root . src/web/pages/settings/UpdateCenterSection.test.tsx src/server/services/updateCenterVersionService.test.ts src/server/routes/api/updateCenter.test.ts src/server/services/updateCenterStatusService.test.ts; npm run typecheck; npm run repo:drift-check; git diff --cached --check origin/main
Not-tested: Full npm test.
This merge keeps PR #533 aligned with dev after addressing CodeRabbit follow-up test comments. The changes are limited to UpdateCenterSection test assertions.
Constraint: Release branch remains the publication branch for the open main-targeting PR.
Confidence: high
Scope-risk: narrow
Tested: npx vitest run --root . src/web/pages/settings/UpdateCenterSection.test.tsx
Tested: npm run typecheck:web
Co-authored-by: OmX <omx@oh-my-codex.dev>
CodeRabbit follow-up review pointed out that one renderer assertion could not fail usefully and that the new recent-candidate deploy test did not prove log streaming after dispatch. The test now asserts the visible candidate text directly and covers streamUpdateCenterTaskLogs for the new deploy path.
Constraint: Keep review follow-up scoped to test coverage only.
Confidence: high
Scope-risk: narrow
Tested: npx vitest run --root . src/web/pages/settings/UpdateCenterSection.test.tsx
Tested: npm run typecheck:web
Co-authored-by: OmX <omx@oh-my-codex.dev>
The release branch is main-based and tracks the reviewed dev slice for PR publication. This merge carries the post-review compatibility fixes without rewriting the already-open PR history.
Constraint: PR #533 publishes dev changes through a clean release branch targeting main.
Rejected: Recreate the PR branch from scratch | unnecessary churn after green checks and review context already exist.
Confidence: high
Scope-risk: narrow
Tested: npx vitest run --root . src/server/proxy-core/responsesPreflight.test.ts src/server/proxy-core/serviceTierPolicy.test.ts src/server/routes/proxy/chat.stream.test.ts src/web/pages/settings/UpdateCenterSection.test.tsx
Tested: npm run typecheck:server
Tested: npm run typecheck:web
Tested: npm run repo:drift-check
Co-authored-by: OmX <omx@oh-my-codex.dev>
Review feedback found narrow compatibility regressions in the web-search shim, Responses tool detection, service tier accounting, and update-center Docker candidate actions. The fix keeps synthetic search routed through the dedicated search model, preserves Responses text compatibility, distinguishes absent service_tier from filtered service_tier, and evaluates recent Docker tags per candidate.
Constraint: Review fixes must stay on dev first, then flow into the main-based release PR branch.
Rejected: Leave Responses web_search simulation on the request model | it has the same search-route selection risk as Anthropic simulation.
Confidence: high
Scope-risk: narrow
Tested: npx vitest run --root . src/server/proxy-core/responsesPreflight.test.ts src/server/proxy-core/serviceTierPolicy.test.ts src/server/routes/proxy/chat.stream.test.ts src/web/pages/settings/UpdateCenterSection.test.tsx
Tested: npm run typecheck:server
Tested: npm run typecheck:web
Tested: npm run repo:drift-check
Co-authored-by: OmX <omx@oh-my-codex.dev>
Several downstream clients send non-cache compatibility fields across Chat, Responses, Anthropic Messages, and Responses WebSocket surfaces. The proxy now normalizes legacy functions/function_call, maps web_search tools across protocols, rejects unsupported HTTP Responses continuation shapes before upstream dispatch, and applies configurable service_tier policy consistently before requests leave the server.
Constraint: Publication flow keeps day-to-day commits on dev and opens main PRs from a release branch.
Constraint: No new dependencies; web_search-only simulation reuses the existing local search route.
Rejected: Forward external HTTP previous_response_id to strict upstreams | it produces ambiguous continuation failures and stale retry behavior outside WebSocket state.
Rejected: Add a dedicated search dependency | existing /v1/search route is sufficient for synthetic server-tool responses.
Confidence: high
Scope-risk: moderate
Directive: Keep Responses HTTP continuation checks aligned with WebSocket continuation support before relaxing these guards.
Tested: npm run typecheck:server
Tested: npx vitest run --root . src/server/proxy-core/responsesPreflight.test.ts src/server/proxy-core/serviceTierPolicy.test.ts src/server/test-fixtures/protocol/non-cache-field-matrix.test.ts src/server/transformers/openai/responses/conversion.test.ts src/server/transformers/anthropic/messages/conversion.test.ts src/server/routes/proxy/chat.stream.test.ts src/server/routes/proxy/responses.codex-oauth.test.ts
Tested: npx vitest run --root . src/server/routes/proxy/responses.websocket.test.ts
Tested: npm run repo:drift-check
Not-tested: Full application build and full Vitest suite
Co-authored-by: OmX <omx@oh-my-codex.dev>
MySQL information_schema.COLUMNS.COLUMN_DEFAULT stores DEFAULT '' as an
empty string, not NULL. normalizeDefaultValueForColumn was treating any
empty rawDefaultValue as 'no default' and returning null, causing the
MySQL schema parity test to see defaultValue: null for post_refresh_probe_model
while schemaContract.json (generated from TypeScript schema) had "''".
Fix: when rawDefaultValue is empty string, return "''" for text/json columns
instead of null, so the round-trip through MySQL introspection matches the
contract.
Better-SQLite3 requires --> statement-breakpoint between each statement
in a migration file; without it all statements are passed as a single
string to Database.prepare() which throws RangeError on multi-statement input.