@bitkyc08/opencodex 2.60.0 → 2.61.0-preview.20260922
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +5 -1
- package/src/adapters/anthropic.ts +16 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +10 -2
- package/src/adapters/command-code.ts +2 -1
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +13 -3
- package/src/adapters/cursor.ts +11 -2
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin.ts +75 -23
- package/src/adapters/google-antigravity-wire.ts +5 -2
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google.ts +29 -5
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat.ts +18 -57
- package/src/adapters/openai-responses/passthrough.ts +2 -0
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +10 -0
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +43 -4
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/index.ts +200 -61
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-listener.ts +2 -5
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/provider-models.ts +24 -15
- package/src/codex/catalog/retained-sync.ts +2 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/history-provider.ts +12 -1
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +52 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +87 -44
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +5 -41
- package/src/codex/shim-templates.ts +29 -3
- package/src/companion/settings.ts +132 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/load-degrade.ts +34 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/schema/config-schema.ts +27 -1
- package/src/config/schema/leaf-validators.ts +47 -0
- package/src/config.ts +1 -1
- package/src/generated/compatibility-version.json +418 -174
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +124 -18
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +49 -11
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug.ts +40 -0
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +59 -14
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/index.ts +24 -1
- package/src/oauth/login-cli.ts +80 -29
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/key-failover.ts +31 -1
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +124 -37
- package/src/providers/quota/vendor-probes-key.ts +37 -22
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +9 -4
- package/src/providers/registry/entries-extended.ts +29 -4
- package/src/providers/registry/model-seeds.ts +47 -10
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/schema.ts +3 -0
- package/src/router.ts +17 -2
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +55 -2
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +55 -17
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +56 -10
- package/src/server/index/spend-ledger-lifecycle.ts +34 -8
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index.ts +21 -28
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +6 -0
- package/src/server/management/shared.ts +18 -1
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +8 -9
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/relay.ts +19 -2
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +114 -0
- package/src/server/request-metrics.ts +92 -30
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +90 -0
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +14 -1
- package/src/server/responses/passthrough-dispatch.ts +179 -35
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/request-prepare.ts +42 -1
- package/src/server/responses/request-send-budget.ts +12 -0
- package/src/server/responses/request-transport.ts +24 -4
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +4 -1
- package/src/server/search.ts +25 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +1 -1
- package/src/types/config.ts +37 -0
- package/src/types/provider.ts +73 -0
- package/src/types/request.ts +28 -2
- package/src/types/tools.ts +19 -0
- package/src/types.ts +3 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/web-search/alpha-search.ts +21 -1
- package/gui/dist/assets/index-BTuCbqQd.css +0 -1
- package/gui/dist/assets/index-DoBVdPHP.js +0 -134
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-path for provider key material: env references, keychain references, or the literal
|
|
3
|
+
* value.
|
|
4
|
+
*
|
|
5
|
+
* Leaf module on purpose: reasoning-metadata.ts imports resolveProviderApiKey() here so a
|
|
6
|
+
* learned-refusal identity can hash the same credential the wire sends. key-store.ts needs
|
|
7
|
+
* the ../config barrel for the write path (saveConfigPreservingClaudeCode), and importing it
|
|
8
|
+
* from reasoning-metadata would close the cycle the reasoning-metadata header warns about.
|
|
9
|
+
* resolveEnvValue comes from ../config/proxy-env for the same reason.
|
|
10
|
+
*
|
|
11
|
+
* `config.json` keeps only a reference (`keychain:<provider>` for the active key,
|
|
12
|
+
* `keychain:<provider>/<pool id>` for pool entries); the secret lives in the OS credential store
|
|
13
|
+
* under one service name. Reads are synchronous on purpose: `routedProviderConfig` and the
|
|
14
|
+
* quota/compaction/catalog callers are all sync, and `@napi-rs/keyring` ships a sync `Entry`.
|
|
15
|
+
*
|
|
16
|
+
* Policy: a reference that cannot be resolved fails closed (no key) and is warned once per
|
|
17
|
+
* account; nothing ever rewrites plaintext into config or its backups.
|
|
18
|
+
*/
|
|
19
|
+
import { createRequire } from "node:module";
|
|
20
|
+
import { resolveEnvValue } from "../config/proxy-env";
|
|
21
|
+
import type { OcxProviderConfig } from "../types";
|
|
22
|
+
|
|
23
|
+
export const KEYCHAIN_REFERENCE_PREFIX = "keychain:";
|
|
24
|
+
export const PROVIDER_KEYCHAIN_SERVICE = "opencodex.provider-api-key.v1";
|
|
25
|
+
|
|
26
|
+
export interface ProviderKeychainEntry {
|
|
27
|
+
getPassword(): string | null;
|
|
28
|
+
setPassword(password: string): void;
|
|
29
|
+
deletePassword(): boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ProviderKeychainEntryFactory = (service: string, account: string) => ProviderKeychainEntry;
|
|
33
|
+
|
|
34
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
35
|
+
|
|
36
|
+
function defaultEntryFactory(service: string, account: string): ProviderKeychainEntry {
|
|
37
|
+
const { Entry } = nodeRequire("@napi-rs/keyring") as { Entry: new (s: string, a: string) => ProviderKeychainEntry };
|
|
38
|
+
return new Entry(service, account);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let entryFactory: ProviderKeychainEntryFactory = defaultEntryFactory;
|
|
42
|
+
const resolvedCache = new Map<string, string>();
|
|
43
|
+
const warnedAccounts = new Set<string>();
|
|
44
|
+
|
|
45
|
+
/** Test seam: swap the OS entry for an in-memory one and drop caches. */
|
|
46
|
+
export function setProviderKeychainEntryFactoryForTests(factory: ProviderKeychainEntryFactory | null): void {
|
|
47
|
+
entryFactory = factory ?? defaultEntryFactory;
|
|
48
|
+
resolvedCache.clear();
|
|
49
|
+
warnedAccounts.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Write-path seam: a store/restore mutated secrets, so cached reads and warnings are stale. */
|
|
53
|
+
export function invalidateResolvedProviderKeyCache(): void {
|
|
54
|
+
resolvedCache.clear();
|
|
55
|
+
warnedAccounts.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isKeychainReference(value: string | undefined): value is string {
|
|
59
|
+
return typeof value === "string" && value.startsWith(KEYCHAIN_REFERENCE_PREFIX) && value.length > KEYCHAIN_REFERENCE_PREFIX.length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function keychainAccount(reference: string): string {
|
|
63
|
+
return reference.slice(KEYCHAIN_REFERENCE_PREFIX.length);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A reference belongs to `name` only when its account is that provider's own active account
|
|
68
|
+
* or one of its pool accounts. `storeProviderKeyInKeychain` writes exactly those two shapes,
|
|
69
|
+
* so anything else in a provider's config names another provider's secret.
|
|
70
|
+
*/
|
|
71
|
+
export function keychainReferenceBelongsToProvider(reference: string, name: string): boolean {
|
|
72
|
+
const account = keychainAccount(reference);
|
|
73
|
+
return account === name || account.startsWith(`${name}/`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Entry for `account` under the provider-key service; read and write paths share the factory. */
|
|
77
|
+
export function providerKeychainEntry(account: string): ProviderKeychainEntry {
|
|
78
|
+
return entryFactory(PROVIDER_KEYCHAIN_SERVICE, account);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readKeychain(account: string): string | undefined {
|
|
82
|
+
const cached = resolvedCache.get(account);
|
|
83
|
+
if (cached !== undefined) return cached;
|
|
84
|
+
try {
|
|
85
|
+
const value = providerKeychainEntry(account).getPassword();
|
|
86
|
+
if (typeof value === "string" && value.trim()) {
|
|
87
|
+
resolvedCache.set(account, value);
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// fall through to the single warning below
|
|
92
|
+
}
|
|
93
|
+
if (!warnedAccounts.has(account)) {
|
|
94
|
+
warnedAccounts.add(account);
|
|
95
|
+
console.warn(`[opencodex] provider key reference keychain:${account} could not be read from the OS keychain; requests for this provider have no credential until the keychain is available (no plaintext fallback)`);
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Single resolver for provider key material: env references, keychain references, or the
|
|
102
|
+
* literal value. Every request-time read of `apiKey` goes through here.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveProviderApiKey(value: string | undefined): string | undefined {
|
|
105
|
+
if (!value) return undefined;
|
|
106
|
+
if (isKeychainReference(value)) return readKeychain(keychainAccount(value));
|
|
107
|
+
return resolveEnvValue(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type ProviderKeyStoreKind = "keychain" | "env" | "file" | "none";
|
|
111
|
+
|
|
112
|
+
export function providerKeyStoreKind(provider: Pick<OcxProviderConfig, "apiKey"> | undefined): ProviderKeyStoreKind {
|
|
113
|
+
const key = provider?.apiKey;
|
|
114
|
+
if (!key) return "none";
|
|
115
|
+
if (isKeychainReference(key)) return "keychain";
|
|
116
|
+
if (/^\$\{?\w+\}?$/.test(key)) return "env";
|
|
117
|
+
return "file";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Probe the OS keychain with a throwaway account: write, read back, delete. */
|
|
121
|
+
export function probeProviderKeychain(): { available: true } | { available: false; reason: string } {
|
|
122
|
+
const account = `probe-${process.pid}-${Date.now()}`;
|
|
123
|
+
try {
|
|
124
|
+
const entry = providerKeychainEntry(account);
|
|
125
|
+
entry.setPassword("ok");
|
|
126
|
+
const back = entry.getPassword();
|
|
127
|
+
try { entry.deletePassword(); } catch { /* best effort */ }
|
|
128
|
+
if (back !== "ok") return { available: false, reason: "keychain read-back did not match" };
|
|
129
|
+
return { available: true };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return { available: false, reason: error instanceof Error ? error.message : "keychain unavailable" };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mutatePersistedConfig } from "../config";
|
|
3
3
|
import { publishAccountSelection } from "../lib/account-selection-events";
|
|
4
|
+
import { clearModelCache } from "../codex/model-cache";
|
|
4
5
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
5
6
|
import type { ProviderApiKeySelection } from "../types/provider";
|
|
6
7
|
import { routedProviderConfig } from "../router";
|
|
@@ -100,6 +101,9 @@ export function commitProviderApiKeySelection<T>(
|
|
|
100
101
|
if (outcome.status === "unavailable") return { status: "unavailable" };
|
|
101
102
|
const committed = outcome.value;
|
|
102
103
|
if (committed.status !== "unavailable") config.providers[name] = structuredClone(committed.provider);
|
|
103
|
-
if (committed.status === "committed" && committed.notify)
|
|
104
|
+
if (committed.status === "committed" && committed.notify) {
|
|
105
|
+
clearModelCache(name);
|
|
106
|
+
publishAccountSelection(name, "api-key");
|
|
107
|
+
}
|
|
104
108
|
return committed;
|
|
105
109
|
}
|
|
@@ -13,7 +13,7 @@ import type { ProviderApiKeySelection } from "../types/provider";
|
|
|
13
13
|
import { routedProviderConfig } from "../router";
|
|
14
14
|
import { getProviderRegistryEntry } from "./registry";
|
|
15
15
|
import { normalizedBaseUrl } from "./quota/vendor-probes-key";
|
|
16
|
-
import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy, TransientRetryPolicy } from "../types";
|
|
16
|
+
import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy, ResetReplayPolicy, TransientRetryPolicy } from "../types";
|
|
17
17
|
import { OPENCODE_GO_SESSION_HEADER } from "./opencode-go-transport";
|
|
18
18
|
import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
|
|
19
19
|
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
@@ -326,6 +326,16 @@ const DEFAULT_TRANSIENT_RETRY = {
|
|
|
326
326
|
attempts: 3,
|
|
327
327
|
} as const satisfies Required<TransientRetryPolicy>;
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Default used when a provider opts in with a bare `retryOnReset: {}`: one replacement for
|
|
331
|
+
* the whole logical request. `replacements` counts duplicate inferences the operator accepts,
|
|
332
|
+
* not retries and not sends.
|
|
333
|
+
*/
|
|
334
|
+
const DEFAULT_RESET_REPLAY = {
|
|
335
|
+
enabled: true,
|
|
336
|
+
replacements: 1,
|
|
337
|
+
} as const satisfies Required<ResetReplayPolicy>;
|
|
338
|
+
|
|
329
339
|
/** Map<`${providerName}\0${keyId}`, KeyCooldown> */
|
|
330
340
|
const keyCooldowns = new Map<string, KeyCooldown>();
|
|
331
341
|
|
|
@@ -616,6 +626,26 @@ export function transientRetryPolicyFor(
|
|
|
616
626
|
};
|
|
617
627
|
}
|
|
618
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Normalize a provider's `retryOnReset` policy, or return null when it is absent or explicitly
|
|
631
|
+
* disabled.
|
|
632
|
+
*
|
|
633
|
+
* No auth-mode gate: the canonical ChatGPT backend is `forward` auth and is the send this
|
|
634
|
+
* policy exists for. Whether a given REQUEST may be replaced is a per-body decision made in
|
|
635
|
+
* src/server/responses/reset-replay.ts, and how many replacements the request gets is the
|
|
636
|
+
* shared allowance on its execution budget. This function only reads the operator's intent.
|
|
637
|
+
*/
|
|
638
|
+
export function resetReplayPolicyFor(
|
|
639
|
+
provider: Pick<OcxProviderConfig, "retryOnReset">,
|
|
640
|
+
): Required<ResetReplayPolicy> | null {
|
|
641
|
+
const policy = provider.retryOnReset;
|
|
642
|
+
if (!policy || policy.enabled === false) return null;
|
|
643
|
+
return {
|
|
644
|
+
enabled: policy.enabled ?? DEFAULT_RESET_REPLAY.enabled,
|
|
645
|
+
replacements: policy.replacements ?? DEFAULT_RESET_REPLAY.replacements,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
619
649
|
/**
|
|
620
650
|
* Wait before the next same-target replay: upstream Retry-After (seconds or HTTP-date) when
|
|
621
651
|
* `respectRetryAfter` is on and the header parses, capped at `maxIntervalMs`; otherwise the
|
|
@@ -1,7 +1,34 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
import { resolveEnvValue, saveConfigPreservingClaudeCode } from "../config";
|
|
3
2
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
4
3
|
import type { ProviderRegistryEntry } from "./registry";
|
|
4
|
+
import {
|
|
5
|
+
KEYCHAIN_REFERENCE_PREFIX,
|
|
6
|
+
invalidateResolvedProviderKeyCache,
|
|
7
|
+
isKeychainReference,
|
|
8
|
+
keychainAccount,
|
|
9
|
+
keychainReferenceBelongsToProvider,
|
|
10
|
+
probeProviderKeychain,
|
|
11
|
+
providerKeychainEntry,
|
|
12
|
+
} from "./api-key-resolve";
|
|
13
|
+
|
|
14
|
+
// The read-path (reference predicates, the OS entry factory, resolveProviderApiKey, the probe
|
|
15
|
+
// and the kind classification) lives in ./api-key-resolve -- a leaf module that
|
|
16
|
+
// reasoning-metadata can also import without pulling the ../config barrel into its
|
|
17
|
+
// cycle-sensitive graph. Re-exported here so existing key-store consumers keep working.
|
|
18
|
+
export {
|
|
19
|
+
KEYCHAIN_REFERENCE_PREFIX,
|
|
20
|
+
PROVIDER_KEYCHAIN_SERVICE,
|
|
21
|
+
isKeychainReference,
|
|
22
|
+
probeProviderKeychain,
|
|
23
|
+
providerKeyStoreKind,
|
|
24
|
+
resolveProviderApiKey,
|
|
25
|
+
setProviderKeychainEntryFactoryForTests,
|
|
26
|
+
} from "./api-key-resolve";
|
|
27
|
+
export type {
|
|
28
|
+
ProviderKeychainEntry,
|
|
29
|
+
ProviderKeychainEntryFactory,
|
|
30
|
+
ProviderKeyStoreKind,
|
|
31
|
+
} from "./api-key-resolve";
|
|
5
32
|
|
|
6
33
|
/** Shared with routing: a key-mode override is effective only while its key resolves. */
|
|
7
34
|
export function providerUsesKeyAuthOverride(
|
|
@@ -27,109 +54,8 @@ export function providerUsesKeyAuthOverride(
|
|
|
27
54
|
* (headless service, locked session) refuses rather than half-migrating.
|
|
28
55
|
*/
|
|
29
56
|
|
|
30
|
-
export const KEYCHAIN_REFERENCE_PREFIX = "keychain:";
|
|
31
|
-
export const PROVIDER_KEYCHAIN_SERVICE = "opencodex.provider-api-key.v1";
|
|
32
|
-
|
|
33
|
-
export interface ProviderKeychainEntry {
|
|
34
|
-
getPassword(): string | null;
|
|
35
|
-
setPassword(password: string): void;
|
|
36
|
-
deletePassword(): boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type ProviderKeychainEntryFactory = (service: string, account: string) => ProviderKeychainEntry;
|
|
40
|
-
|
|
41
|
-
const nodeRequire = createRequire(import.meta.url);
|
|
42
|
-
|
|
43
|
-
function defaultEntryFactory(service: string, account: string): ProviderKeychainEntry {
|
|
44
|
-
const { Entry } = nodeRequire("@napi-rs/keyring") as { Entry: new (s: string, a: string) => ProviderKeychainEntry };
|
|
45
|
-
return new Entry(service, account);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let entryFactory: ProviderKeychainEntryFactory = defaultEntryFactory;
|
|
49
|
-
const resolvedCache = new Map<string, string>();
|
|
50
|
-
const warnedAccounts = new Set<string>();
|
|
51
|
-
|
|
52
|
-
/** Test seam: swap the OS entry for an in-memory one and drop caches. */
|
|
53
|
-
export function setProviderKeychainEntryFactoryForTests(factory: ProviderKeychainEntryFactory | null): void {
|
|
54
|
-
entryFactory = factory ?? defaultEntryFactory;
|
|
55
|
-
resolvedCache.clear();
|
|
56
|
-
warnedAccounts.clear();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function isKeychainReference(value: string | undefined): value is string {
|
|
60
|
-
return typeof value === "string" && value.startsWith(KEYCHAIN_REFERENCE_PREFIX) && value.length > KEYCHAIN_REFERENCE_PREFIX.length;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function keychainAccount(reference: string): string {
|
|
64
|
-
return reference.slice(KEYCHAIN_REFERENCE_PREFIX.length);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* A reference belongs to `name` only when its account is that provider's own active account
|
|
69
|
-
* or one of its pool accounts. `storeProviderKeyInKeychain` writes exactly those two shapes,
|
|
70
|
-
* so anything else in a provider's config names another provider's secret.
|
|
71
|
-
*/
|
|
72
|
-
function keychainReferenceBelongsToProvider(reference: string, name: string): boolean {
|
|
73
|
-
const account = keychainAccount(reference);
|
|
74
|
-
return account === name || account.startsWith(`${name}/`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function readKeychain(account: string): string | undefined {
|
|
78
|
-
const cached = resolvedCache.get(account);
|
|
79
|
-
if (cached !== undefined) return cached;
|
|
80
|
-
try {
|
|
81
|
-
const value = entryFactory(PROVIDER_KEYCHAIN_SERVICE, account).getPassword();
|
|
82
|
-
if (typeof value === "string" && value.trim()) {
|
|
83
|
-
resolvedCache.set(account, value);
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
// fall through to the single warning below
|
|
88
|
-
}
|
|
89
|
-
if (!warnedAccounts.has(account)) {
|
|
90
|
-
warnedAccounts.add(account);
|
|
91
|
-
console.warn(`[opencodex] provider key reference keychain:${account} could not be read from the OS keychain; requests for this provider have no credential until the keychain is available (no plaintext fallback)`);
|
|
92
|
-
}
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Single resolver for provider key material: env references, keychain references, or the
|
|
98
|
-
* literal value. Every request-time read of `apiKey` goes through here.
|
|
99
|
-
*/
|
|
100
|
-
export function resolveProviderApiKey(value: string | undefined): string | undefined {
|
|
101
|
-
if (!value) return undefined;
|
|
102
|
-
if (isKeychainReference(value)) return readKeychain(keychainAccount(value));
|
|
103
|
-
return resolveEnvValue(value);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export type ProviderKeyStoreKind = "keychain" | "env" | "file" | "none";
|
|
107
|
-
|
|
108
|
-
export function providerKeyStoreKind(provider: Pick<OcxProviderConfig, "apiKey"> | undefined): ProviderKeyStoreKind {
|
|
109
|
-
const key = provider?.apiKey;
|
|
110
|
-
if (!key) return "none";
|
|
111
|
-
if (isKeychainReference(key)) return "keychain";
|
|
112
|
-
if (/^\$\{?\w+\}?$/.test(key)) return "env";
|
|
113
|
-
return "file";
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Probe the OS keychain with a throwaway account: write, read back, delete. */
|
|
117
|
-
export function probeProviderKeychain(): { available: true } | { available: false; reason: string } {
|
|
118
|
-
const account = `probe-${process.pid}-${Date.now()}`;
|
|
119
|
-
try {
|
|
120
|
-
const entry = entryFactory(PROVIDER_KEYCHAIN_SERVICE, account);
|
|
121
|
-
entry.setPassword("ok");
|
|
122
|
-
const back = entry.getPassword();
|
|
123
|
-
try { entry.deletePassword(); } catch { /* best effort */ }
|
|
124
|
-
if (back !== "ok") return { available: false, reason: "keychain read-back did not match" };
|
|
125
|
-
return { available: true };
|
|
126
|
-
} catch (error) {
|
|
127
|
-
return { available: false, reason: error instanceof Error ? error.message : "keychain unavailable" };
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
57
|
function writeVerified(account: string, secret: string): void {
|
|
132
|
-
const entry =
|
|
58
|
+
const entry = providerKeychainEntry(account);
|
|
133
59
|
entry.setPassword(secret);
|
|
134
60
|
if (entry.getPassword() !== secret) throw new Error(`keychain read-back mismatch for ${account}`);
|
|
135
61
|
}
|
|
@@ -177,13 +103,12 @@ export function storeProviderKeyInKeychain(config: OcxConfig, name: string): { o
|
|
|
177
103
|
}
|
|
178
104
|
} catch (error) {
|
|
179
105
|
for (const account of written) {
|
|
180
|
-
try {
|
|
106
|
+
try { providerKeychainEntry(account).deletePassword(); } catch { /* best effort */ }
|
|
181
107
|
}
|
|
182
108
|
return { ok: false, error: `OS keychain write failed: ${error instanceof Error ? error.message : "unknown"}`, status: 503 };
|
|
183
109
|
}
|
|
184
110
|
for (const apply of planned) apply();
|
|
185
|
-
|
|
186
|
-
warnedAccounts.clear();
|
|
111
|
+
invalidateResolvedProviderKeyCache();
|
|
187
112
|
saveConfigPreservingClaudeCode(config);
|
|
188
113
|
return { ok: true, moved: written.length };
|
|
189
114
|
}
|
|
@@ -211,7 +136,7 @@ export function restoreProviderKeyFromKeychain(config: OcxConfig, name: string):
|
|
|
211
136
|
const account = keychainAccount(ref);
|
|
212
137
|
if (resolved.has(account)) continue;
|
|
213
138
|
let value: string | null = null;
|
|
214
|
-
try { value =
|
|
139
|
+
try { value = providerKeychainEntry(account).getPassword(); } catch { value = null; }
|
|
215
140
|
if (!value) return { ok: false, error: `OS keychain has no readable secret for ${ref}; config left unchanged`, status: 503 };
|
|
216
141
|
resolved.set(account, value);
|
|
217
142
|
}
|
|
@@ -220,10 +145,9 @@ export function restoreProviderKeyFromKeychain(config: OcxConfig, name: string):
|
|
|
220
145
|
}
|
|
221
146
|
if (isKeychainReference(provider.apiKey)) provider.apiKey = resolved.get(keychainAccount(provider.apiKey))!;
|
|
222
147
|
for (const account of resolved.keys()) {
|
|
223
|
-
try {
|
|
148
|
+
try { providerKeychainEntry(account).deletePassword(); } catch { /* best effort */ }
|
|
224
149
|
}
|
|
225
|
-
|
|
226
|
-
warnedAccounts.clear();
|
|
150
|
+
invalidateResolvedProviderKeyCache();
|
|
227
151
|
saveConfigPreservingClaudeCode(config);
|
|
228
152
|
return { ok: true, restored: resolved.size };
|
|
229
153
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
// Every provider field receives a deliberate identity role. A new field fails typecheck
|
|
4
|
+
// until its model-id semantics are classified; credentials and provider-wide policy are none.
|
|
5
|
+
type ModelRenameRole = "none" | "record" | "list" | "scalar" | "nested-models";
|
|
6
|
+
export const PROVIDER_MODEL_RENAME_ROLES = {
|
|
7
|
+
alias: "none",
|
|
8
|
+
modelAliases: "record",
|
|
9
|
+
modelDisplayNames: "record",
|
|
10
|
+
defaultAliases: "none",
|
|
11
|
+
adapter: "none",
|
|
12
|
+
codexToolMode: "none",
|
|
13
|
+
requestPacing: "nested-models",
|
|
14
|
+
mcpMaxTools: "none",
|
|
15
|
+
mcpMaxSchemaBytes: "none",
|
|
16
|
+
mcpMaxResultBytes: "none",
|
|
17
|
+
modelAdapters: "record",
|
|
18
|
+
fastWire: "none",
|
|
19
|
+
baseUrl: "none",
|
|
20
|
+
responsesPath: "none",
|
|
21
|
+
chatCompletionsPath: "none",
|
|
22
|
+
commandCodeVersion: "none",
|
|
23
|
+
statelessResponses: "none",
|
|
24
|
+
requiresAdjacentResponsesToolResults: "none",
|
|
25
|
+
requiresPairedResponsesToolResults: "none",
|
|
26
|
+
annotateEmptyToolOutputs: "none",
|
|
27
|
+
supportsServiceTier: "none",
|
|
28
|
+
modelSupportsServiceTier: "record",
|
|
29
|
+
preserveResponsesReasoningContent: "none",
|
|
30
|
+
modelReasoningEffortsAuthoritative: "none",
|
|
31
|
+
dropResponsesReasoningItems: "none",
|
|
32
|
+
decodesNativeCompactionBlobs: "none",
|
|
33
|
+
allowEncryptedV2AgentTasks: "none",
|
|
34
|
+
allowPrivateNetwork: "none",
|
|
35
|
+
proxy: "none",
|
|
36
|
+
noProxy: "none",
|
|
37
|
+
upstreamHttpVersion: "none",
|
|
38
|
+
upstreamWebsocket: "none",
|
|
39
|
+
directGeminiWireRenames: "none",
|
|
40
|
+
disabled: "none",
|
|
41
|
+
codexAccountMode: "none",
|
|
42
|
+
apiKey: "none",
|
|
43
|
+
apiKeyTransport: "none",
|
|
44
|
+
apiKeyPool: "none",
|
|
45
|
+
apiKeyPoolStrategy: "none",
|
|
46
|
+
apiKeySelectionRevision: "none",
|
|
47
|
+
_apiKeyAttempt: "none",
|
|
48
|
+
defaultModel: "scalar",
|
|
49
|
+
models: "list",
|
|
50
|
+
liveModels: "none",
|
|
51
|
+
selectedModels: "list",
|
|
52
|
+
initialModelSelection: "none",
|
|
53
|
+
retainModels: "list",
|
|
54
|
+
newModelPolicy: "none",
|
|
55
|
+
modelPreset: "none",
|
|
56
|
+
contextWindow: "none",
|
|
57
|
+
modelContextWindows: "record",
|
|
58
|
+
modelInputModalities: "record",
|
|
59
|
+
modelCapabilities: "record",
|
|
60
|
+
modelMaxInputTokens: "record",
|
|
61
|
+
modelAutoCompactTokenLimits: "record",
|
|
62
|
+
defaultMaxOutputTokens: "none",
|
|
63
|
+
modelMaxOutputTokens: "record",
|
|
64
|
+
modelCosts: "record",
|
|
65
|
+
autoReviewModel: "none",
|
|
66
|
+
autoReviewModelOverrides: "record",
|
|
67
|
+
headers: "none",
|
|
68
|
+
openRouterRouting: "none",
|
|
69
|
+
modelOpenRouterRouting: "record",
|
|
70
|
+
vercelGatewayRouting: "none",
|
|
71
|
+
modelVercelGatewayRouting: "record",
|
|
72
|
+
authMode: "none",
|
|
73
|
+
oauthAccountFailover: "none",
|
|
74
|
+
keyOptional: "none",
|
|
75
|
+
freeTier: "none",
|
|
76
|
+
note: "none",
|
|
77
|
+
modelSuffixBracketStrip: "none",
|
|
78
|
+
refreshPolicy: "none",
|
|
79
|
+
reasoningEfforts: "none",
|
|
80
|
+
modelReasoningEfforts: "record",
|
|
81
|
+
modelSuppressSyntheticMax: "record",
|
|
82
|
+
modelDefaultReasoningEfforts: "record",
|
|
83
|
+
pinnedReasoningEffort: "none",
|
|
84
|
+
modelPinnedReasoningEfforts: "record",
|
|
85
|
+
modelSupportsReasoningSummaries: "record",
|
|
86
|
+
modelSupportsVerbosity: "record",
|
|
87
|
+
supportsVerbosity: "none",
|
|
88
|
+
modelReasoningSummaryDelivery: "record",
|
|
89
|
+
modelPreferHostedTools: "record",
|
|
90
|
+
supportsOpenAiWebSearchToolFields: "none",
|
|
91
|
+
xaiResponsesXSearch: "none",
|
|
92
|
+
xaiResponsesDefaultVersion: "none",
|
|
93
|
+
zaiResponsesDefaultVersion: "none",
|
|
94
|
+
supportsResponsesCustomTools: "none",
|
|
95
|
+
unsupportedHostedTools: "none",
|
|
96
|
+
responsesSnapshotRepair: "none",
|
|
97
|
+
webSearchBridge: "none",
|
|
98
|
+
reasoningEffortMap: "none",
|
|
99
|
+
modelReasoningEffortMap: "record",
|
|
100
|
+
reasoningWireFormat: "none",
|
|
101
|
+
noReasoningModels: "list",
|
|
102
|
+
noTemperatureModels: "list",
|
|
103
|
+
noTopPModels: "list",
|
|
104
|
+
noPenaltyModels: "list",
|
|
105
|
+
noStructuredOutputModels: "list",
|
|
106
|
+
noJsonSchemaModels: "list",
|
|
107
|
+
omitReasoningEffortWithToolsModels: "list",
|
|
108
|
+
parallelToolCalls: "none",
|
|
109
|
+
pinParallelToolCallsFalse: "none",
|
|
110
|
+
terminalContinuationGuard: "none",
|
|
111
|
+
openaiChatEofTolerance: "none",
|
|
112
|
+
foldDeveloperRoleToSystem: "none",
|
|
113
|
+
promptCacheKey: "none",
|
|
114
|
+
chatServiceTier: "none",
|
|
115
|
+
responsesItemIdRepair: "none",
|
|
116
|
+
autoToolChoiceOnlyModels: "list",
|
|
117
|
+
preserveReasoningContentModels: "list",
|
|
118
|
+
requiresReasoningPlaceholderModels: "list",
|
|
119
|
+
showThinkingSummary: "none",
|
|
120
|
+
retryOn429: "none",
|
|
121
|
+
transientRetryOn5xx: "none",
|
|
122
|
+
retryOnReset: "none",
|
|
123
|
+
reasoningSplitModels: "list",
|
|
124
|
+
reasoningDetailsModels: "list",
|
|
125
|
+
thinkingToggleModels: "list",
|
|
126
|
+
thinkingBudgetModels: "list",
|
|
127
|
+
escapeBuiltinToolNames: "none",
|
|
128
|
+
anthropicEofTolerance: "none",
|
|
129
|
+
noVisionModels: "list",
|
|
130
|
+
googleMode: "none",
|
|
131
|
+
googleToolSchemaPolicy: "none",
|
|
132
|
+
project: "none",
|
|
133
|
+
location: "none",
|
|
134
|
+
mcpServers: "none",
|
|
135
|
+
desktopExecutor: "none",
|
|
136
|
+
unsafeAllowNativeLocalExec: "none",
|
|
137
|
+
nativeLocalExec: "none",
|
|
138
|
+
} as const satisfies Record<keyof OcxProviderConfig, ModelRenameRole>;
|
|
139
|
+
|
|
140
|
+
function fieldsWithRole(role: ModelRenameRole): string[] {
|
|
141
|
+
return Object.entries(PROVIDER_MODEL_RENAME_ROLES).filter(([, value]) => value === role).map(([key]) => key);
|
|
142
|
+
}
|
|
143
|
+
export const MODEL_KEYED_RECORDS = fieldsWithRole("record");
|
|
144
|
+
// Legacy persisted registry field, no longer part of the public provider config interface.
|
|
145
|
+
export const MODEL_ID_LISTS = [...fieldsWithRole("list"), "directReasoningEffortModels"];
|
|
146
|
+
export const MODEL_ID_SCALARS = fieldsWithRole("scalar");
|
|
147
|
+
export const MODEL_NESTED_RECORDS = fieldsWithRole("nested-models");
|