Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,45 @@ provider-scoped or ladder-inferred.

`tests/codex-catalog.test.ts` is also touched by PR #1119. If that PR lands
first, rebase onto it rather than duplicating its cases.

## What audit changed after implementation

The first implementation fixed the canonical provider ids and passed its tests,
and was still wrong about the reported case. Recording why, because the failure
mode generalizes.

`enrichProviderFromRegistry` matches on the provider NAME. The reporter's row is
a hand-added provider literally called `GLM`. Routing worked, so nothing looked
broken — but no registry id is called `GLM`, so the metadata never arrived. The
tests substituted canonical ids (`zai`, `zhipu-bigmodel`) and were green against
a configuration no user had.

Fix: on the name-lookup miss, fall back to
`registryEntryForProviderDestination`, which matches by vendor endpoint and is
already restricted to fixed key destinations.

Two further corrections from the same audit:

- The fallback originally bailed whenever the user had any map, recreating the
whole-record bug the per-key merge was written to prevent.
- `enrichProviderFromCatalog` persists what it enriches, so registry defaults
were being frozen into saved config as user overrides.

## Deferred: the reporter's exact endpoint

`https://open.bigmodel.cn/api/coding/paas/v4` appears in no registry entry —
only `/api/paas/v4` does, as `zhipu-bigmodel`. The coding path exists solely in
`FREE_PROVIDER_DIRECTORY` as `glm-cn`.

Closing that route needs a new registry entry, and the audit confirmed it would
be safe with a distinct id (`glm` and `glm-cn` are both already bound, and
reusing either would retarget an existing config's endpoint — the warning at
`registry.ts:1668-1676`). It also needs `preserveCustomDestination: true`, its
own evidence-backed model set rather than the pay-as-you-go GLM 4.6–5.1
metadata, and updates to `EXPECTED_KEY_PROVIDER_IDS` in
`tests/provider-registry-parity.test.ts`.

That is a provider addition, not a bug fix. It stays out of this stack
deliberately: the destination fallback already fixes every custom-named row on
an endpoint we know, and mixing a new vendor entry into a bug-fix chain would
expand the review surface past what a reviewer can check in one pass.
Comment on lines +136 to +153

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the endpoint status in this audit record.

This section says that https://open.bigmodel.cn/api/coding/paas/v4 has no registry entry and remains deferred. src/providers/registry.ts now adds zhipu-bigmodel-coding for that exact endpoint.

Replace the deferred-state statements with the implemented outcome. Keep the original audit rationale if it is useful for history.

🤖 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 `@devlog/_plan/260807_untouched_bug_stack/020_routed_reasoning_effort.md`
around lines 136 - 153, Update the “Deferred: the reporter's exact endpoint”
section to reflect that registry.ts now includes the zhipu-bigmodel-coding entry
for https://open.bigmodel.cn/api/coding/paas/v4. Remove statements claiming the
endpoint lacks a registry entry or remains deferred, while preserving the
historical rationale where useful and documenting the implemented provider
outcome.

18 changes: 16 additions & 2 deletions src/lib/windows-secret-acl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,23 @@ export interface HardenOptions {
* timeout retry and the diagnostic verification pass (no per-attempt fresh budget:
* loadConfig hardens dir+config+auth sequentially, so per-attempt budgets stack
* into multi-minute startup stalls). Override with OPENCODEX_ACL_TIMEOUT_MS
* (integer ms, clamped to [1000, 60000]; invalid values fall back to 5000).
* (integer ms, clamped to [1000, 60000]; invalid values fall back to 30000).
*
* The default was 5s until #1156. One envelope has to cover the whole sequence —
* `/grant:r`, `/inheritance:r`, `/remove:g`, plus the conditional `/findsid`
* verification — and on machines where icacls is slow (Defender real-time scanning,
* roaming profiles, a domain-controller round trip) 5s ran out mid-sequence. The
* harden then failed closed, the native-main owner published a permanent
* `unavailable`, and every native request returned 503 until restart. A slow start
* is recoverable; that is not.
*
* The cost is honest and worth stating: because loadConfig hardens three paths
* sequentially, the timeout-path worst case at load is ~90s, and the owner path
* (initial call + one recovery) is ~60.25s. Both require icacls to be
* pathologically slow on every call; a healthy machine finishes in milliseconds
* and sees no change. Operators who prefer the old bound can set the env override.
*/
const HARDEN_DEADLINE_DEFAULT_MS = 5_000;
const HARDEN_DEADLINE_DEFAULT_MS = 30_000;
const HARDEN_DEADLINE_MIN_MS = 1_000;
const HARDEN_DEADLINE_MAX_MS = 60_000;

Expand Down
12 changes: 12 additions & 0 deletions src/oauth/key-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,21 @@ export const KEY_LOGIN_PROVIDERS: Record<string, KeyLoginProvider> = deriveKeyLo
* `noReasoningModels`, `defaultModel`) onto a provider config being created, for any field the
* caller didn't already supply. Lets the vision/reasoning classification actually reach the saved
* config (the GUI/API only send adapter/baseUrl/apiKey/defaultModel). No-op for unknown names.
*
* `modelSupportsReasoningSummaries` is deliberately excluded from what gets persisted. It is
* registry-only metadata resolved at runtime, and this function feeds a config that is about to
* be written to disk. Persisting today's registry defaults would freeze them as the user's own
* overrides: a later registry correction — say we learn a model's backend rejects summary
* delivery — would never reach anyone who created their provider before the correction, and they
* would keep getting upstream 400s with no way to know why. Catalog gathering enriches a
* detached runtime clone, so the defaults still apply where they matter.
*/
export function enrichProviderFromCatalog(name: string, prov: OcxProviderConfig): void {
const hadOwnSummaries = Object.hasOwn(prov, "modelSupportsReasoningSummaries");
const submittedSummaries = prov.modelSupportsReasoningSummaries;
enrichProviderFromRegistry(name, prov);
if (hadOwnSummaries) prov.modelSupportsReasoningSummaries = submittedSummaries;
else delete prov.modelSupportsReasoningSummaries;
}

export function isKeyLoginProvider(name: string): boolean {
Expand Down
50 changes: 49 additions & 1 deletion src/providers/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CodexAccountMode, OcxProviderConfig } from "../types";
import {
PROVIDER_REGISTRY,
providerMatchesRegistryTransport,
registryEntryForProviderDestination,
type ProviderRegistryEntry,
} from "./registry";

Expand Down Expand Up @@ -243,9 +244,55 @@ export function deriveProviderPresets(): DerivedProviderPreset[] {
return [...dedupePresets(presets), customPreset()];
}

/**
* Merge registry reasoning-summary defaults PER KEY, letting explicit user values win.
*
* Not a whole-Record `=== undefined` fill like the scalars around it: a user who sets one
* model's flag creates a defined Record, and a whole-object check would then suppress every
* registry default for that provider. Spreading registry-first also preserves an explicit
* `false` — someone who disabled summaries for a model because their backend 400s on it keeps
* that. The result is a fresh object, so saved config never aliases the registry constant.
*/
function applyReasoningSummaryDefaults(
prov: OcxProviderConfig,
defaults: Readonly<Record<string, boolean>> | undefined,
): void {
if (!defaults) return;
prov.modelSupportsReasoningSummaries = {
...defaults,
...(prov.modelSupportsReasoningSummaries ?? {}),
};
}

/**
* Last-resort enrichment for a provider whose NAME matches no registry id.
*
* #1100 was reported against a hand-added provider called "GLM" pointing at a vendor endpoint
* we recognize. Routing worked, so the row looked healthy, but every piece of registry metadata
* was skipped and the reasoning ladder was advertised without summary support — exactly the
* inconsistency that makes Codex drop the inbound reasoning object.
*
* Deliberately narrow: only the reasoning-summary map, and only via
* `registryEntryForProviderDestination`, which matches fixed key destinations and refuses
* templated or overridable base URLs. A custom row keeps its own identity for everything else.
*/
function enrichReasoningSummariesByDestination(prov: OcxProviderConfig): void {
const destination = registryEntryForProviderDestination(prov);
applyReasoningSummaryDefaults(prov, destination?.modelSupportsReasoningSummaries);
}

export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig): void {
const entry = PROVIDER_REGISTRY.find(row => row.id === name);
if (!entry || !providerMatchesRegistryTransport(name, prov)) return;
if (!entry || !providerMatchesRegistryTransport(name, prov)) {
// Name lookup failed, but the row may still point at a vendor route we know. #1100 was
// reported against a hand-added provider literally named "GLM": routing worked, yet every
// piece of registry metadata was skipped because no registry id is called "GLM".
// `registryEntryForProviderDestination` answers the question that actually matters here —
// which vendor endpoint is this row talking to — and is already restricted to fixed key
// destinations, so a templated or overridable base URL cannot be claimed by it.
enrichReasoningSummariesByDestination(prov);
return;
}
const seed = providerConfigSeed(entry);
if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
Expand Down Expand Up @@ -280,6 +327,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
// the entry so an explicit user value stays distinguishable from the default.
if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
// Registry-only repair policy (#938): fill only when the runtime provider has
// no explicit policy, and deep-clone so saved/user values never alias the
// registry constant.
Expand Down
49 changes: 49 additions & 0 deletions src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ export interface ProviderRegistryEntry {
supportsServiceTier?: boolean;
/** Registry default for plaintext reasoning replay; see `OcxProviderConfig.preserveResponsesReasoningContent`. Registry-only like `supportsServiceTier`. */
preserveResponsesReasoningContent?: boolean;
/** Registry defaults for per-model Codex reasoning propagation; explicit user keys win during enrichment. */
modelSupportsReasoningSummaries?: Record<string, boolean>;
modelDiscovery?: ProviderModelDiscoverySpec;
contextWindow?: number;
modelContextWindows?: Record<string, number>;
Expand Down Expand Up @@ -1104,6 +1106,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP])),
...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekReasoningMapFor(id)])),
},
modelSupportsReasoningSummaries: {
"glm-5.2": true,
"glm-5.1": true,
"glm-5": true,
...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, true])),
},
thinkingToggleModels: OPENCODE_GO_THINKING_TOGGLE_MODELS,
thinkingBudgetModels: THINKING_BUDGET_MODELS,
noReasoningModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
Expand Down Expand Up @@ -1340,6 +1348,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
*/
modelReasoningEfforts: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),
modelReasoningEffortMap: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekReasoningMapFor(id)])),
modelSupportsReasoningSummaries: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, true])),
preserveReasoningContentModels: DEEPSEEK_THINKING_MODELS,
// Issue #88: every DeepSeek API model is text-only input (no image support upstream) — the
// vision sidecar describes attached images for them, and the catalog advertises image input
Expand Down Expand Up @@ -1653,6 +1662,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
modelSuffixBracketStrip: true,
noVisionModels: ZAI_GLM_52_MODELS,
modelReasoningEfforts: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_EFFORTS])),
modelSupportsReasoningSummaries: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, true])),
preserveReasoningContentModels: ZAI_GLM_52_MODELS,
},
// Zhipu's domestic BigModel platform: OpenAI-compatible pay-as-you-go on open.bigmodel.cn — a
Expand Down Expand Up @@ -1689,11 +1699,50 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
modelReasoningEffortMap: Object.fromEntries(
ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP]),
),
modelSupportsReasoningSummaries: Object.fromEntries(
ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS.map(id => [id, true]),
),
preserveReasoningContentModels: ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS,
// No liveModels: GET /api/paas/v4/models has not been observed to answer on this host, and a
// false live claim yields an empty picker at runtime. Flip it on once someone verifies it.
note: "Domestic BigModel pay-as-you-go endpoint (open.bigmodel.cn)",
},
// BigModel's Coding Plan is a SEPARATE endpoint from the pay-as-you-go row above, and that is
// the whole reason this one exists. #1100 was reported against
// `https://open.bigmodel.cn/api/coding/paas/v4`; the row above covers only `/api/paas/v4`, so
// destination enrichment matched nothing, `modelSupportsReasoningSummaries` stayed unset, and
// Codex kept dropping the inbound reasoning object — effort displayed as `-`.
//
// A prefix or fuzzy endpoint match would have been the shortcut. It is also how a config
// pointed at one vendor route silently inherits another route's metadata, so endpoints stay
// exact and each one gets its own row.
//
// The id is NOT `glm-cn`, which the free-provider directory already binds to this same coding
// path: registering it here would let routedProviderConfig() canonicalize a saved `glm-cn`
// config onto this baseUrl. Same reasoning as `zhipu-bigmodel` above.
//
// Models follow Z.AI's coding-plan list rather than the pay-as-you-go one. This endpoint is
// the subscription product, and the reporter's `glm-5.2` is only on that side.
{
id: "zhipu-bigmodel-coding",
label: "Zhipu AI — BigModel Coding Plan",
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
adapter: "openai-chat",
authKind: "key",
dashboardUrl: "https://bigmodel.cn/console/usercenter/apikeys",
Comment on lines +1727 to +1732

Copy link
Copy Markdown
Contributor

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

Preserve custom destinations for this new fixed-route provider.

preserveCustomDestination is absent. Therefore, providerMatchesRegistryTransport() does not verify the adapter and endpoint for an existing provider named zhipu-bigmodel-coding.

A custom provider with this name and another destination can receive Coding Plan models and reasoning-summary defaults. That can enable summary delivery for an upstream that rejects it.

Set preserveCustomDestination: true. This entry has a fixed key endpoint and no base-URL override.

Proposed fix
   {
     id: "zhipu-bigmodel-coding",
     label: "Zhipu AI — BigModel Coding Plan",
     baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
     adapter: "openai-chat",
     authKind: "key",
+    preserveCustomDestination: true,

As per path instructions, flag provider/adapter contract drift in src/**.

📝 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
id: "zhipu-bigmodel-coding",
label: "Zhipu AI — BigModel Coding Plan",
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
adapter: "openai-chat",
authKind: "key",
dashboardUrl: "https://bigmodel.cn/console/usercenter/apikeys",
id: "zhipu-bigmodel-coding",
label: "Zhipu AI — BigModel Coding Plan",
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
adapter: "openai-chat",
authKind: "key",
preserveCustomDestination: true,
dashboardUrl: "https://bigmodel.cn/console/usercenter/apikeys",
🤖 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/registry.ts` around lines 1727 - 1732, Update the
`zhipu-bigmodel-coding` registry entry to set `preserveCustomDestination: true`,
ensuring `providerMatchesRegistryTransport()` validates its fixed adapter and
endpoint for existing providers with the same name. Keep the key-based
authentication and fixed base URL unchanged.

Source: Path instructions

defaultModel: "glm-5.2",
models: ["glm-5.2", "glm-5.2[1m]", "glm-5.1", "glm-5", "glm-4.6"],
jawcodeBundle: "zai",
modelContextWindows: { "glm-5.2": 1_000_000, "glm-5.2[1m]": 1_000_000 },
modelSuffixBracketStrip: true,
noVisionModels: ZAI_GLM_52_MODELS,
modelReasoningEfforts: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, ZAI_GLM_52_REASONING_EFFORTS])),
modelSupportsReasoningSummaries: Object.fromEntries(ZAI_GLM_52_MODELS.map(id => [id, true])),
preserveReasoningContentModels: ZAI_GLM_52_MODELS,
// No liveModels: the same reasoning as the pay-as-you-go row — an unverified live claim
// yields an empty picker at runtime.
note: "Domestic BigModel Coding Plan endpoint (open.bigmodel.cn)",
},
{ id: "nanogpt", label: "NanoGPT", baseUrl: "https://nano-gpt.com/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://nano-gpt.com/api" },
{ id: "synthetic", label: "Synthetic", baseUrl: "https://api.synthetic.new/openai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://synthetic.new" },
// SiliconFlow publishes an OpenAI-compatible chat endpoint and a dynamic model catalog. Do not
Expand Down
Loading
Loading