@bitkyc08/opencodex 2.84.0 → 2.86.0
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/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { isLoopbackHostname, shouldInjectApiAuthHeader } from "../../codex/loopback-target";
|
|
4
|
+
import { formatSelectorConjunction } from "../../integrations/merge";
|
|
5
|
+
import { ClientPathError } from "../config-export";
|
|
6
|
+
import type { ExportContext, ManagedContribution, OpencodeLaunchEnv } from "./contracts";
|
|
7
|
+
import { LOOPBACK_API_KEY_PLACEHOLDER, OPENCODE_PROVIDER_ID } from "./constants";
|
|
8
|
+
import { authoritativeContextWindow, normalizeExportModels } from "./model-metadata";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Qoder's configuration directory: `QODER_CONFIG_DIR` when set, otherwise `~/.qoder`
|
|
12
|
+
* (https://docs.qoder.com/cli/settings-reference). Export and detection both resolve it here.
|
|
13
|
+
* A relative override is refused because opencodex and Qoder would anchor it to different cwds.
|
|
14
|
+
*/
|
|
15
|
+
export function qoderHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
16
|
+
const override = env.QODER_CONFIG_DIR?.trim();
|
|
17
|
+
if (!override) return join(home, ".qoder");
|
|
18
|
+
if (override === "~") return home;
|
|
19
|
+
if (override.startsWith("~/") || override.startsWith("~\\")) return join(home, override.slice(2));
|
|
20
|
+
if (!isAbsolute(override)) {
|
|
21
|
+
throw new ClientPathError(
|
|
22
|
+
`QODER_CONFIG_DIR must be an absolute path or start with ~; "${override}" depends on the working directory, `
|
|
23
|
+
+ "so opencodex and Qoder would disagree about which file it names.",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return override;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function qoderConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
30
|
+
return join(qoderHomeDir(env, home), "settings.json");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Qoder IDE and qodercli share this provider and custom-model catalog. */
|
|
34
|
+
export function buildQoderClientConfig(ctx: ExportContext) {
|
|
35
|
+
if (shouldInjectApiAuthHeader(ctx.config) || !isLoopbackHostname(new URL(ctx.baseUrl).hostname)) {
|
|
36
|
+
throw new Error("Qoder's placeholder API key requires a loopback-only listener.");
|
|
37
|
+
}
|
|
38
|
+
return qoderDocument(ctx);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function qoderDocument(ctx: ExportContext) {
|
|
42
|
+
const models = normalizeExportModels(ctx.models);
|
|
43
|
+
return {
|
|
44
|
+
providers: {
|
|
45
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
46
|
+
type: "openai-compatible",
|
|
47
|
+
protocol: "openai",
|
|
48
|
+
displayName: "opencodex",
|
|
49
|
+
baseUrl: ctx.baseUrl,
|
|
50
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
51
|
+
...(models[0] ? { defaultModel: models[0].namespaced } : {}),
|
|
52
|
+
models: models.map(model => ({ model: model.namespaced })),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
modelConfigs: {
|
|
56
|
+
customModels: models.map(model => ({
|
|
57
|
+
provider: OPENCODE_PROVIDER_ID,
|
|
58
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
59
|
+
model: model.namespaced,
|
|
60
|
+
baseURL: ctx.baseUrl,
|
|
61
|
+
displayName: model.namespaced,
|
|
62
|
+
maxInputTokens: authoritativeContextWindow(model.contextWindow) ?? 128000,
|
|
63
|
+
})),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type QoderGeneratedConfig = ReturnType<typeof buildQoderClientConfig>;
|
|
69
|
+
|
|
70
|
+
export function summarizeQoder(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
71
|
+
const models = (document as QoderGeneratedConfig | undefined)?.modelConfigs?.customModels ?? [];
|
|
72
|
+
return { modelCount: models.length, modelsWithoutLimits: 0 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildQoderContribution(ctx: ExportContext): ManagedContribution {
|
|
76
|
+
// The writer gates admission before applying. Classification and disable must
|
|
77
|
+
// still work after the operator changes the proxy to a non-loopback bind.
|
|
78
|
+
const doc = qoderDocument(ctx);
|
|
79
|
+
return {
|
|
80
|
+
clientId: "qoder",
|
|
81
|
+
fragments: [
|
|
82
|
+
{ path: ["providers", OPENCODE_PROVIDER_ID], value: doc.providers[OPENCODE_PROVIDER_ID] },
|
|
83
|
+
...doc.modelConfigs.customModels.map(model => {
|
|
84
|
+
const selector = formatSelectorConjunction([
|
|
85
|
+
{ field: "provider", value: OPENCODE_PROVIDER_ID },
|
|
86
|
+
{ field: "model", value: model.model },
|
|
87
|
+
]);
|
|
88
|
+
if (selector === null) throw new Error("Qoder model ID cannot be represented by a managed array selector.");
|
|
89
|
+
return { path: ["modelConfigs", "customModels", selector], value: model };
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -24,6 +24,7 @@ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "nod
|
|
|
24
24
|
import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
|
|
25
25
|
import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
|
|
26
26
|
import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
|
|
27
|
+
import { fileIO } from "../integrations/config-io";
|
|
27
28
|
import { canonicalizeReasoningEfforts } from "../reasoning-effort";
|
|
28
29
|
import { expandFastExportModels } from "./config-export/fast-models";
|
|
29
30
|
import { probeHostname } from "../server/proxy-liveness";
|
|
@@ -81,6 +82,8 @@ import {
|
|
|
81
82
|
|
|
82
83
|
export type { CommandCodeGeneratedConfig, CommandCodeModelEntry, CommandCodeProviderBlock };
|
|
83
84
|
export { buildCommandCodeClientConfig, summarizeCommandCode, buildCommandCodeContribution, commandCodeProviderRoot };
|
|
85
|
+
import { buildQoderClientConfig, summarizeQoder, buildQoderContribution, qoderConfigPath } from "./config-export/qoder";
|
|
86
|
+
export { buildQoderClientConfig, qoderConfigPath, qoderHomeDir, type QoderGeneratedConfig } from "./config-export/qoder";
|
|
84
87
|
|
|
85
88
|
|
|
86
89
|
|
|
@@ -475,10 +478,8 @@ export type ZcodePathIO = {
|
|
|
475
478
|
readText: (path: string) => { kind: "text"; text: string } | { kind: "missing" } | { kind: "failed"; code?: string };
|
|
476
479
|
};
|
|
477
480
|
const zcodePathIO: ZcodePathIO = {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? { kind: "missing" } : { kind: "failed" }; }
|
|
481
|
-
},
|
|
481
|
+
/** Shares the integration reader's four-MiB limit and regular-file-only bootstrap contract. */
|
|
482
|
+
readText: path => fileIO().readText(path),
|
|
482
483
|
};
|
|
483
484
|
export class ZcodeSettingsPathError extends ClientPathError {}
|
|
484
485
|
|
|
@@ -1429,6 +1430,18 @@ export function clineSettingsDir(env: OpencodeLaunchEnv = process.env, home: str
|
|
|
1429
1430
|
}
|
|
1430
1431
|
|
|
1431
1432
|
export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
1433
|
+
qoder: {
|
|
1434
|
+
id: "qoder",
|
|
1435
|
+
filename: "qoder-settings.json",
|
|
1436
|
+
destination: env => qoderConfigPath(env),
|
|
1437
|
+
apiKeyEnv: "",
|
|
1438
|
+
exportHint: "Qoder reads a non-secret placeholder from settings.json; loopback needs no key.",
|
|
1439
|
+
build: buildQoderClientConfig,
|
|
1440
|
+
format: "json",
|
|
1441
|
+
summarize: summarizeQoder,
|
|
1442
|
+
buildContribution: buildQoderContribution,
|
|
1443
|
+
loopbackOnly: true,
|
|
1444
|
+
},
|
|
1432
1445
|
opencode: {
|
|
1433
1446
|
id: "opencode",
|
|
1434
1447
|
filename: "opencode.json",
|
|
@@ -25,6 +25,7 @@ import type { PoolQuotaResult } from "./pool-quota-probe";
|
|
|
25
25
|
import { fetchMainAccountInfoAttempt, fetchMainAccountInfo, MAIN_CACHE_TTL } from "./main-account-probe";
|
|
26
26
|
import { fetchPoolAccountQuota, PoolQuotaProbeBusyError, POOL_CACHE_TTL, POOL_QUOTA_REFRESH_CONCURRENCY } from "./pool-quota-probe";
|
|
27
27
|
import { getRuntimeConfig, configuredPoolAccount, mapWithConcurrency } from "./runtime-config";
|
|
28
|
+
import { trackBackgroundWork } from "../../lib/background-work";
|
|
28
29
|
|
|
29
30
|
let primeInFlight: Promise<void> | null = null;
|
|
30
31
|
let lastPriorityFailbackPrimeAt: number | undefined;
|
|
@@ -259,7 +260,9 @@ export async function primeCodexPoolQuotas(
|
|
|
259
260
|
&& now - lastPriorityFailbackPrimeAt < CODEX_PRIORITY_FAILBACK_REFRESH_MS) return;
|
|
260
261
|
lastPriorityFailbackPrimeAt = now;
|
|
261
262
|
}
|
|
262
|
-
|
|
263
|
+
// Tracked because most callers do not await it, yet it holds the native-main claim
|
|
264
|
+
// database open; whoever removes that directory settles it first (see background-work).
|
|
265
|
+
primeInFlight = trackBackgroundWork(`codex-pool-quota-prime:${reason}`, (async () => {
|
|
263
266
|
const pool = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
264
267
|
const stale = pool.filter(a => {
|
|
265
268
|
const q = getAccountQuota(a.id);
|
|
@@ -344,7 +347,7 @@ export async function primeCodexPoolQuotas(
|
|
|
344
347
|
if (process.env.OPENCODEX_DEBUG_QUOTA === "1") {
|
|
345
348
|
console.warn(`[codex-quota] prime done (reason=${reason}, pool=${pool.length}, refreshed=${stale.length})`);
|
|
346
349
|
}
|
|
347
|
-
})().finally(() => { primeInFlight = null; });
|
|
350
|
+
})().finally(() => { primeInFlight = null; }));
|
|
348
351
|
return primeInFlight;
|
|
349
352
|
}
|
|
350
353
|
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
// The half-open TRANSIENT-HOLD lease (#4701). Not the quota-cooldown probe lease imported from
|
|
52
52
|
// ./routing above -- different module, different domain, and a request never holds both.
|
|
53
53
|
import { releaseTransientProbe } from "../routing/probe-lease";
|
|
54
|
+
import { trackBackgroundWork } from "../lib/background-work";
|
|
54
55
|
import {
|
|
55
56
|
codexConversationIdentity,
|
|
56
57
|
recordCodexThreadLineage,
|
|
@@ -66,7 +67,8 @@ import {
|
|
|
66
67
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./catalog/native-models";
|
|
67
68
|
import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
|
|
68
69
|
import { maskAccountId } from "../lib/privacy";
|
|
69
|
-
import { formatErrorResponse } from "../bridge";
|
|
70
|
+
import { formatErrorResponse, formatUsageLimitReachedResponse } from "../bridge";
|
|
71
|
+
import { nextCodexPoolRecoveryAt } from "./routing/pool-recovery";
|
|
70
72
|
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, getMainPolicyQuota, parseUsageQuota, parseMainPolicyUsageQuota, setAccountQuotaFromParsed } from "./quota";
|
|
71
73
|
import { codexAccountUsesCreditsAfterLimit, codexUsageLimitResetAt } from "./account-credit-use";
|
|
72
74
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
@@ -523,6 +525,11 @@ export class CodexAccountCooldownError extends Error {
|
|
|
523
525
|
|
|
524
526
|
export class CodexMainAccountHardLockError extends CodexAccountCooldownError {
|
|
525
527
|
readonly resetAt?: number;
|
|
528
|
+
/**
|
|
529
|
+
* Earliest known recovery of another stored pool account, set only when an unpinned pool
|
|
530
|
+
* selection reached main (#7046). A hint for the client's retry time; never routing input.
|
|
531
|
+
*/
|
|
532
|
+
poolRecoveryAt?: number;
|
|
526
533
|
|
|
527
534
|
constructor(resetAt?: number, thresholds = resolveMainAccountHardLockThresholds(undefined)) {
|
|
528
535
|
super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
|
|
@@ -935,16 +942,40 @@ export function cooldownErrorMessage(err: CodexAccountCooldownError, accountSele
|
|
|
935
942
|
+ ` (source: ${err.cooldownSource ?? "default"}).${recovery}`;
|
|
936
943
|
}
|
|
937
944
|
|
|
945
|
+
/**
|
|
946
|
+
* The soonest a main hard-lock refusal is worth retrying: a sibling pool account's known
|
|
947
|
+
* recovery when it comes first, otherwise main's own reset (#7046). A pool hint is held to at
|
|
948
|
+
* least {@link MAIN_HARD_LOCK_MIN_POOL_RETRY_MS} so a hold about to lapse cannot turn the refusal
|
|
949
|
+
* into a tight retry loop. Null when neither time is known and still in the future.
|
|
950
|
+
*/
|
|
951
|
+
export const MAIN_HARD_LOCK_MIN_POOL_RETRY_MS = 60_000;
|
|
952
|
+
export function mainHardLockRetryAt(err: CodexMainAccountHardLockError, now: number): number | null {
|
|
953
|
+
const future = (value: number | undefined): number | null =>
|
|
954
|
+
typeof value === "number" && Number.isFinite(value) && value > now ? value : null;
|
|
955
|
+
const main = future(err.resetAt);
|
|
956
|
+
const pool = future(err.poolRecoveryAt);
|
|
957
|
+
const poolRetryAt = pool === null ? null : Math.max(pool, now + MAIN_HARD_LOCK_MIN_POOL_RETRY_MS);
|
|
958
|
+
if (poolRetryAt !== null && (main === null || poolRetryAt < main)) return poolRetryAt;
|
|
959
|
+
return main;
|
|
960
|
+
}
|
|
961
|
+
|
|
938
962
|
/** HTTP form of {@link cooldownErrorMessage}, carrying Retry-After for well-behaved clients. */
|
|
939
963
|
export function cooldownErrorResponse(
|
|
940
964
|
err: CodexAccountCooldownError,
|
|
941
965
|
now = Date.now(),
|
|
942
966
|
accountSelector?: string,
|
|
943
967
|
): Response {
|
|
968
|
+
if (err instanceof CodexMainAccountHardLockError) {
|
|
969
|
+
// A local policy refusal with a known horizon, not a transient throttle (#7046): Codex keeps
|
|
970
|
+
// a Goal active and sleeps through `rate_limit_exceeded` for the whole Retry-After, but
|
|
971
|
+
// stops on `usage_limit_reached` and shows the reset. The guard itself is unchanged.
|
|
972
|
+
const retryAt = mainHardLockRetryAt(err, now);
|
|
973
|
+
return formatUsageLimitReachedResponse(cooldownErrorMessage(err, accountSelector), retryAt,
|
|
974
|
+
retryAt === null ? null : Math.max(1, Math.ceil((retryAt - now) / 1000)));
|
|
975
|
+
}
|
|
944
976
|
const res = formatErrorResponse(429, "rate_limit_error", cooldownErrorMessage(err, accountSelector));
|
|
945
977
|
const headers = new Headers(res.headers);
|
|
946
|
-
if (!(err instanceof CodexReserveUnavailableError)
|
|
947
|
-
&& (!(err instanceof CodexMainAccountHardLockError) || err.resetAt !== undefined)) {
|
|
978
|
+
if (!(err instanceof CodexReserveUnavailableError)) {
|
|
948
979
|
headers.set("Retry-After", String(Math.max(1, Math.ceil((err.cooldownUntil - now) / 1000))));
|
|
949
980
|
}
|
|
950
981
|
return new Response(res.body, { status: res.status, headers });
|
|
@@ -1044,11 +1075,51 @@ async function awaitMainAccountPolicyBindingSettled(
|
|
|
1044
1075
|
if (!settled) throw new CodexMainProfileDrainingError();
|
|
1045
1076
|
}
|
|
1046
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* Resolve the credential for one Codex turn. A main hard-lock reached through unpinned pool
|
|
1080
|
+
* selection is annotated with the earliest known pool recovery, so the refusal can name a retry
|
|
1081
|
+
* time that a sibling account may meet (#7046).
|
|
1082
|
+
*/
|
|
1047
1083
|
export async function resolveCodexAuthContext(
|
|
1048
1084
|
headers: Headers,
|
|
1049
1085
|
config: OcxConfig,
|
|
1050
1086
|
mode: CodexAccountMode,
|
|
1051
1087
|
options: ResolveCodexAuthContextOptions = {},
|
|
1088
|
+
): Promise<CodexAuthContext> {
|
|
1089
|
+
// Filled by the inner resolver with the request's own fenced model evidence, so the hint obeys it.
|
|
1090
|
+
const eligibility: RecoveryHintEligibility = {};
|
|
1091
|
+
try {
|
|
1092
|
+
return await resolveCodexAuthContextUnannotated(headers, config, mode, options, eligibility);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
if (error instanceof CodexMainAccountHardLockError && mode === "pool" && options.accountId === undefined
|
|
1095
|
+
&& options.excludeAccountId !== MAIN_CODEX_ACCOUNT_ID
|
|
1096
|
+
&& !requiresReserveAuthorization(options.codexAuthPolicy ?? config, options.modelId, options.admission)
|
|
1097
|
+
// An account-gated model with no resolved roster has no known eligible sibling.
|
|
1098
|
+
&& (!options.modelId || !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
|
|
1099
|
+
|| eligibility.modelEligibleAccountIds !== undefined)) {
|
|
1100
|
+
try {
|
|
1101
|
+
const now = Date.now();
|
|
1102
|
+
error.poolRecoveryAt = nextCodexPoolRecoveryAt(config, now, codexQuotaScopeForModel(options.modelId), {
|
|
1103
|
+
deniedAccountIds: eligibility.deniedModelAccountIds,
|
|
1104
|
+
eligibleAccountIds: eligibility.modelEligibleAccountIds,
|
|
1105
|
+
});
|
|
1106
|
+
} catch { /* the hint is optional; the refusal stands without it */ }
|
|
1107
|
+
}
|
|
1108
|
+
throw error;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
interface RecoveryHintEligibility {
|
|
1113
|
+
modelEligibleAccountIds?: ReadonlySet<string>;
|
|
1114
|
+
deniedModelAccountIds?: ReadonlySet<string>;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
async function resolveCodexAuthContextUnannotated(
|
|
1118
|
+
headers: Headers,
|
|
1119
|
+
config: OcxConfig,
|
|
1120
|
+
mode: CodexAccountMode,
|
|
1121
|
+
options: ResolveCodexAuthContextOptions = {},
|
|
1122
|
+
eligibility: RecoveryHintEligibility = {},
|
|
1052
1123
|
): Promise<CodexAuthContext> {
|
|
1053
1124
|
const writerGeneration = captureConfigGeneration();
|
|
1054
1125
|
const policy = options.codexAuthPolicy ?? config;
|
|
@@ -1256,6 +1327,7 @@ export async function resolveCodexAuthContext(
|
|
|
1256
1327
|
const modelEligibleAccountIds = entitledAccountIds
|
|
1257
1328
|
? new Set([...entitledAccountIds].filter(candidate => !excludeAccountIds?.has(candidate)))
|
|
1258
1329
|
: undefined;
|
|
1330
|
+
eligibility.modelEligibleAccountIds = modelEligibleAccountIds;
|
|
1259
1331
|
// #4768: the flagships stay visible and never fail closed, so this is evidence routing may
|
|
1260
1332
|
// ORDER by, not evidence it may refuse on. Read synchronously from rosters discovery has
|
|
1261
1333
|
// already gathered -- no upstream fetch joins the request path for the most commonly
|
|
@@ -1273,6 +1345,7 @@ export async function resolveCodexAuthContext(
|
|
|
1273
1345
|
undefined,
|
|
1274
1346
|
{ excludeAccountIds },
|
|
1275
1347
|
);
|
|
1348
|
+
eligibility.deniedModelAccountIds = deniedModelAccountIds;
|
|
1276
1349
|
const selectionOptions = {
|
|
1277
1350
|
// Temporary switch drain keeps the candidate until the atomic claim rejects
|
|
1278
1351
|
// it. Retained recovery makes main wholly ineligible so pool routing continues.
|
|
@@ -1475,9 +1548,10 @@ export async function resolveCodexAuthContext(
|
|
|
1475
1548
|
if (options.primeCodexPoolQuotas) {
|
|
1476
1549
|
void options.primeCodexPoolQuotas(config, reason).catch(() => {});
|
|
1477
1550
|
} else {
|
|
1478
|
-
import
|
|
1551
|
+
// Tracked from the import on: the prime registers itself only once the module resolves.
|
|
1552
|
+
void trackBackgroundWork(`codex-pool-quota-prime-import:${reason}`, import("./auth-api")
|
|
1479
1553
|
.then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, reason))
|
|
1480
|
-
.catch(() => {});
|
|
1554
|
+
.catch(() => {}));
|
|
1481
1555
|
}
|
|
1482
1556
|
}
|
|
1483
1557
|
// Snapshot (not just the deadline) so a refused request can report WHY it is cooled:
|
|
@@ -35,6 +35,7 @@ export interface StartupHealthInputs {
|
|
|
35
35
|
shimHealthy: boolean;
|
|
36
36
|
platform: NodeJS.Platform;
|
|
37
37
|
diagnosticStale?: boolean;
|
|
38
|
+
shimActivationPending?: boolean;
|
|
38
39
|
routingAdoption?: RoutingAdoptionEvidence;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -55,6 +56,8 @@ export interface StartupHealth {
|
|
|
55
56
|
serviceConflict: boolean;
|
|
56
57
|
shimInstalled: boolean;
|
|
57
58
|
shimHealthy: boolean;
|
|
59
|
+
/** Intact PATH-overlay shim this process could not see active; activate it rather than repair it. */
|
|
60
|
+
shimActivationPending?: boolean;
|
|
58
61
|
shimCoverage: ShimCoverage;
|
|
59
62
|
serviceSupported: boolean;
|
|
60
63
|
platform: NodeJS.Platform;
|
|
@@ -171,6 +174,7 @@ export function collectStartupHealth(
|
|
|
171
174
|
serviceSupported: service.supported,
|
|
172
175
|
shimInstalled: shim.installed,
|
|
173
176
|
shimHealthy: shim.healthy,
|
|
177
|
+
...(shim.activationPending ? { shimActivationPending: true } : {}),
|
|
174
178
|
platform: process.platform,
|
|
175
179
|
...(routingAdoption ? { routingAdoption } : {}),
|
|
176
180
|
});
|
|
@@ -48,7 +48,8 @@ import { NATIVE_RESERVE_MODEL } from "./native-models";
|
|
|
48
48
|
import { isReserveCatalogProjection, type ReserveCatalogProjection } from "./reserve";
|
|
49
49
|
import { deriveEntry, finishUpstreamNativeEntry, isExactComboCatalogEntry } from "./derive-entry";
|
|
50
50
|
import { PICKER_ORDER_PRIORITY_BASE, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
|
|
51
|
-
import {
|
|
51
|
+
import { applyConfiguredUltraFast } from "./configured-tiers";
|
|
52
|
+
import { applyNativeUltraFastTier } from "./native-ultrafast";
|
|
52
53
|
|
|
53
54
|
export interface ObservedCatalogEntryBuildInput {
|
|
54
55
|
readonly template: RawEntry | null;
|
|
@@ -473,6 +474,8 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
|
|
|
473
474
|
export interface ObservedCatalogMergeInput {
|
|
474
475
|
/** Preserve operator-supplied Ultra Fast only from the exact persisted routed row. */
|
|
475
476
|
readonly ultraFastTier?: boolean;
|
|
477
|
+
/** Current captured wire declarations; disk rows and templates never populate this set. */
|
|
478
|
+
readonly configuredUltraFastSlugs?: ReadonlySet<string>;
|
|
476
479
|
readonly catalogModels: readonly RawEntry[];
|
|
477
480
|
readonly baselineCatalogModels: readonly RawEntry[];
|
|
478
481
|
readonly routedEntries: readonly RawEntry[];
|
|
@@ -514,6 +517,7 @@ export interface ObservedCatalogMergeInput {
|
|
|
514
517
|
*/
|
|
515
518
|
export function mergeCatalogEntriesFromObservedState({
|
|
516
519
|
ultraFastTier = false,
|
|
520
|
+
configuredUltraFastSlugs = new Set(),
|
|
517
521
|
catalogModels,
|
|
518
522
|
baselineCatalogModels,
|
|
519
523
|
routedEntries,
|
|
@@ -865,7 +869,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
865
869
|
if (typeof entry.slug !== "string" || !entry.slug.includes("/")
|
|
866
870
|
|| isNativeAliasCatalogEntry(entry) || isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
867
871
|
|| trustedAccountBoundNativeCatalogSlug(entry) !== undefined) continue;
|
|
868
|
-
|
|
872
|
+
applyConfiguredUltraFast(entry, operatorRows.get(entry.slug), configuredUltraFastSlugs.has(entry.slug), ultraFastTier);
|
|
869
873
|
}
|
|
870
874
|
const finalRoutedEntrySet = new Set(finalRoutedEntries);
|
|
871
875
|
const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
|
|
@@ -932,6 +936,11 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
932
936
|
);
|
|
933
937
|
}
|
|
934
938
|
}
|
|
939
|
+
// Native Astra/Sol rows (bare and account-bound) gain or lose the documented Ultrafast
|
|
940
|
+
// choice with the switch; aliases, combos and reserve projections keep their own tiers.
|
|
941
|
+
if (!reserveProjection && !exactCombo && m.owned_by !== COMBO_NAMESPACE && !isNativeAliasCatalogEntry(e)) {
|
|
942
|
+
applyNativeUltraFastTier(e, ultraFastTier);
|
|
943
|
+
}
|
|
935
944
|
if (wsEnabled) e.supports_websockets = true;
|
|
936
945
|
else {
|
|
937
946
|
delete e.supports_websockets;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { COMBO_NAMESPACE } from "../../combos";
|
|
2
|
+
import type { ResolvedFastPolicy } from "../../providers/fastwire";
|
|
3
|
+
import { catalogModelSlug, type CatalogModel, type RawEntry } from "./parsing";
|
|
4
|
+
import { preserveOperatorUltraFastTiers } from "./operator-tiers";
|
|
5
|
+
|
|
6
|
+
const MANAGED_ULTRAFAST = "opencodex_managed_ultrafast";
|
|
7
|
+
|
|
8
|
+
/** Advertise Ultra Fast only when the eligible final wire explicitly maps that exact tier. */
|
|
9
|
+
export function supportsConfiguredUltraFast(policy: ResolvedFastPolicy | undefined): boolean {
|
|
10
|
+
return policy?.eligibility === "eligible"
|
|
11
|
+
&& policy.fastWire?.kind === "service-tier"
|
|
12
|
+
&& policy.fastWire.canonicalToWire.ultrafast === "ultrafast";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Carry captured capability to the pure merge without treating persisted metadata as authority. */
|
|
16
|
+
export function configuredUltraFastSlugs(models: readonly CatalogModel[]): ReadonlySet<string> {
|
|
17
|
+
const declarations = new Map<string, boolean>();
|
|
18
|
+
for (const model of models) {
|
|
19
|
+
const slug = catalogModelSlug(model);
|
|
20
|
+
const supported = model.provider !== COMBO_NAMESPACE && model.nativeAlias !== true
|
|
21
|
+
&& model.supportsServiceTier === true && model.supportsUltraFastTier === true;
|
|
22
|
+
// A conflicting duplicate cannot lend its capability to the surviving row.
|
|
23
|
+
declarations.set(slug, supported && declarations.get(slug) !== false);
|
|
24
|
+
}
|
|
25
|
+
return new Set([...declarations].filter(([, supported]) => supported).map(([slug]) => slug));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Apply current configuration, or retain an independently supplied exact-row operator tier. */
|
|
29
|
+
export function applyConfiguredUltraFast(
|
|
30
|
+
entry: RawEntry,
|
|
31
|
+
previous: RawEntry | undefined,
|
|
32
|
+
configured: boolean,
|
|
33
|
+
retainOperatorTier: boolean,
|
|
34
|
+
): void {
|
|
35
|
+
// This marker only rejects stale generated metadata; it never authorizes a new tier.
|
|
36
|
+
const wasManaged = previous?.[MANAGED_ULTRAFAST] === entry.slug;
|
|
37
|
+
preserveOperatorUltraFastTiers(entry, wasManaged || configured ? undefined : previous, retainOperatorTier);
|
|
38
|
+
delete entry[MANAGED_ULTRAFAST];
|
|
39
|
+
if (!configured) return;
|
|
40
|
+
entry.service_tiers = [...(Array.isArray(entry.service_tiers) ? entry.service_tiers : []), {
|
|
41
|
+
id: "ultrafast",
|
|
42
|
+
name: "Ultra Fast",
|
|
43
|
+
description: "Higher speed and usage; provider access required",
|
|
44
|
+
}];
|
|
45
|
+
entry.additional_speed_tiers = [
|
|
46
|
+
...(Array.isArray(entry.additional_speed_tiers) ? entry.additional_speed_tiers : []), "ultrafast",
|
|
47
|
+
];
|
|
48
|
+
// Advertising a selectable capability does not opt the user into its increased usage.
|
|
49
|
+
entry[MANAGED_ULTRAFAST] = entry.slug;
|
|
50
|
+
}
|
|
@@ -204,6 +204,8 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
|
|
|
204
204
|
* Two real sources exist and both are consulted here, in the same precedence the
|
|
205
205
|
* writers use (`applyCatalogMetadata` runs first, the model's own fields
|
|
206
206
|
* overwrite it): the `CatalogModel` and the generated jawcode metadata table.
|
|
207
|
+
* Vendor-ID fallback windows are estimates for client budgets, recorded separately rather
|
|
208
|
+
* than promoted to serving-provider assertions.
|
|
207
209
|
* Reading only the model would silently drop every provider whose capabilities
|
|
208
210
|
* live in that table.
|
|
209
211
|
*/
|
|
@@ -230,7 +232,8 @@ function stampCapabilityProvenance(entry: RawEntry, model: CatalogModel): void {
|
|
|
230
232
|
entry.opencodex_capability_provenance = {
|
|
231
233
|
provider: model.provider,
|
|
232
234
|
model_id: model.id,
|
|
233
|
-
...(contextWindow !== undefined ? { context_window: contextWindow } : {}),
|
|
235
|
+
...(contextWindow !== undefined && !model.contextWindowInference ? { context_window: contextWindow } : {}),
|
|
236
|
+
...(model.contextWindowInference ? { inferred_context_window: contextWindow, context_window_inference: { ...model.contextWindowInference } } : {}),
|
|
234
237
|
...(inputModalities !== undefined ? { input_modalities: [...inputModalities] } : {}),
|
|
235
238
|
...(Array.isArray(model.capabilities) && model.capabilities.length > 0
|
|
236
239
|
? { capabilities: [...model.capabilities] }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { canInferVendorContext, vendorContextInference } from "./vendor-context";
|
|
1
2
|
import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
|
|
2
3
|
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
4
|
+
import { supportsConfiguredUltraFast } from "./configured-tiers";
|
|
3
5
|
import { execFileSync } from "node:child_process";
|
|
4
6
|
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
5
7
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
@@ -250,6 +252,7 @@ export function configuredReasoningSummarySupport(prov: OcxProviderConfig | unde
|
|
|
250
252
|
return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
|
|
251
253
|
}
|
|
252
254
|
|
|
255
|
+
/** Reapply configured limits and captured wire capabilities without trusting stale discovery hints. */
|
|
253
256
|
export function applyProviderConfigHints(
|
|
254
257
|
name: string,
|
|
255
258
|
prov: OcxProviderConfig,
|
|
@@ -309,7 +312,12 @@ export function applyProviderConfigHints(
|
|
|
309
312
|
// owns catalog publication, including an explicit provider-level false.
|
|
310
313
|
const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy);
|
|
311
314
|
const {
|
|
315
|
+
contextWindowInference: _staleContextInference,
|
|
316
|
+
contextWindow: _previousContextWindow,
|
|
317
|
+
contextCap: previousContextCap,
|
|
318
|
+
contextCapped: previousContextCapped,
|
|
312
319
|
supportsServiceTier: _staleServiceTier,
|
|
320
|
+
supportsUltraFastTier: _staleUltraFastTier,
|
|
313
321
|
fastTierDescription: _staleFastTierDescription,
|
|
314
322
|
providerAlias: _staleProviderAlias,
|
|
315
323
|
suppressSyntheticMax: _staleSuppressSyntheticMax,
|
|
@@ -317,16 +325,23 @@ export function applyProviderConfigHints(
|
|
|
317
325
|
} = model;
|
|
318
326
|
// 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。
|
|
319
327
|
const discoveredWindow = githubCopilotCatalogContextWindow(name, prov, model.id,
|
|
320
|
-
typeof model.contextWindow === "number" && model.contextWindow > 0 ? model.contextWindow : undefined);
|
|
328
|
+
!model.contextWindowInference && typeof model.contextWindow === "number" && model.contextWindow > 0 ? model.contextWindow : undefined);
|
|
321
329
|
const projectedLimits = clampObservedModelLimits(staticPolicy.model, {
|
|
322
330
|
...(discoveredWindow !== undefined ? { contextWindow: discoveredWindow } : {}),
|
|
323
331
|
...(typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? { maxInputTokens: model.maxInputTokens } : {}),
|
|
324
332
|
});
|
|
333
|
+
const azureWindow = positiveSafeInteger(azureMetadata?.contextWindow);
|
|
334
|
+
const inferredWindow = projectedLimits.contextWindow === undefined && azureWindow === undefined && canInferVendorContext(name, prov)
|
|
335
|
+
? vendorContextInference(model.id) : undefined;
|
|
325
336
|
const hintedWindow = projectedLimits.contextWindow
|
|
326
|
-
??
|
|
337
|
+
?? azureWindow
|
|
338
|
+
?? inferredWindow?.contextWindow
|
|
327
339
|
?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined);
|
|
328
340
|
const hinted = {
|
|
329
341
|
...modelWithoutServiceTier,
|
|
342
|
+
...(!model.contextWindowInference && previousContextCap !== undefined
|
|
343
|
+
? { contextCap: previousContextCap, contextCapped: previousContextCapped } : {}),
|
|
344
|
+
...(inferredWindow ? { contextWindowInference: inferredWindow } : {}),
|
|
330
345
|
...(displayName !== undefined ? { displayName } : {}),
|
|
331
346
|
...(providerAlias !== undefined ? { providerAlias } : {}),
|
|
332
347
|
...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
|
|
@@ -343,6 +358,7 @@ export function applyProviderConfigHints(
|
|
|
343
358
|
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
344
359
|
...(typeof supportsVerbosity === "boolean" ? { supportsVerbosity } : {}),
|
|
345
360
|
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
361
|
+
supportsUltraFastTier: supportsConfiguredUltraFast(fastPolicy),
|
|
346
362
|
...(supportsServiceTier === true && fastPolicy.fastTierDescription !== undefined
|
|
347
363
|
? { fastTierDescription: fastPolicy.fastTierDescription }
|
|
348
364
|
: {}),
|
|
@@ -431,7 +447,9 @@ export function suppressedSyntheticMaxCatalogSlugs(
|
|
|
431
447
|
}
|
|
432
448
|
return slugs;
|
|
433
449
|
}
|
|
434
|
-
|
|
450
|
+
// google-antigravity: the static lineup is an entitlement superset (Claude 5.5 and Gemini 4 Argon
|
|
451
|
+
// are per-account), so a live roster that omits some of it is the expected case, not drift.
|
|
452
|
+
export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai", "google-antigravity"]);
|
|
435
453
|
|
|
436
454
|
export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
437
455
|
// CodeBuddy's vendor defaults are real callable selectors — both the bundled manifests
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { RawEntry } from "./parsing";
|
|
2
|
+
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
3
|
+
import { NATIVE_GPT6_ASTRA_MODEL, NATIVE_GPT61_SOL_MODEL } from "./native-models";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The Ultrafast speed choice OpenAI documents for GPT-6 Astra and GPT-6.1 Sol
|
|
7
|
+
* (https://developers.openai.com/api/docs/guides/ultrafast-mode and
|
|
8
|
+
* https://learn.chatgpt.com/docs/agent-configuration/speed, checked 2026-10-13).
|
|
9
|
+
*
|
|
10
|
+
* The upstream bundled catalog lists only `priority` for both models; ChatGPT's live model
|
|
11
|
+
* list adds Ultrafast for eligible plans. Codex stops consulting that live list once
|
|
12
|
+
* `model_catalog_json` points at our generated catalog, so without this row the speed picker
|
|
13
|
+
* never offers Ultrafast. The `ultraFastTier` switch is the operator's statement that their
|
|
14
|
+
* plan is eligible; the description still says the tier requires one.
|
|
15
|
+
*/
|
|
16
|
+
export const NATIVE_ULTRAFAST_SERVICE_TIER: Readonly<{ id: string; name: string; description: string }> = Object.freeze({
|
|
17
|
+
id: "ultrafast",
|
|
18
|
+
name: "Ultrafast",
|
|
19
|
+
description: "Lower latency, increased usage; requires an eligible plan",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const NATIVE_ULTRAFAST_MODELS: ReadonlySet<string> = new Set([NATIVE_GPT6_ASTRA_MODEL, NATIVE_GPT61_SOL_MODEL]);
|
|
23
|
+
|
|
24
|
+
function isUltraFastId(value: unknown): boolean {
|
|
25
|
+
return typeof value === "string" && value.trim().toLowerCase() === "ultrafast";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isUltraFastDescriptor(value: unknown): boolean {
|
|
29
|
+
return value !== null && typeof value === "object" && "id" in value && isUltraFastId((value as { id: unknown }).id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Provenance marker naming the tier fields this module added to a row. Ownership is never
|
|
34
|
+
* inferred from a descriptor's text: an operator may have written the very same entry, and only
|
|
35
|
+
* a row carrying this marker has anything for the switch to withdraw.
|
|
36
|
+
*/
|
|
37
|
+
export const NATIVE_ULTRAFAST_PROVENANCE_FIELD = "opencodex_generated_service_tiers";
|
|
38
|
+
type TierField = "service_tiers" | "additional_speed_tiers";
|
|
39
|
+
|
|
40
|
+
function generatedFields(entry: RawEntry): Set<TierField> {
|
|
41
|
+
const raw = entry[NATIVE_ULTRAFAST_PROVENANCE_FIELD];
|
|
42
|
+
const fields = new Set<TierField>();
|
|
43
|
+
if (Array.isArray(raw)) {
|
|
44
|
+
for (const value of raw) if (value === "service_tiers" || value === "additional_speed_tiers") fields.add(value);
|
|
45
|
+
}
|
|
46
|
+
return fields;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isGeneratedDescriptor(value: unknown): boolean {
|
|
50
|
+
if (!isUltraFastDescriptor(value)) return false;
|
|
51
|
+
const tier = value as Record<string, unknown>;
|
|
52
|
+
return Object.keys(tier).length === 3
|
|
53
|
+
&& tier.id === NATIVE_ULTRAFAST_SERVICE_TIER.id
|
|
54
|
+
&& tier.name === NATIVE_ULTRAFAST_SERVICE_TIER.name
|
|
55
|
+
&& tier.description === NATIVE_ULTRAFAST_SERVICE_TIER.description;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Bare native rows and trusted account-bound clones of the two Ultrafast models. */
|
|
59
|
+
export function nativeUltraFastSlug(entry: RawEntry): string | undefined {
|
|
60
|
+
const slug = trustedAccountBoundNativeCatalogSlug(entry)
|
|
61
|
+
?? (typeof entry.slug === "string" && !entry.slug.includes("/") ? entry.slug : undefined);
|
|
62
|
+
return slug !== undefined && NATIVE_ULTRAFAST_MODELS.has(slug) ? slug : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add (enabled) or withdraw (disabled) the documented Ultrafast choice on an eligible native row.
|
|
67
|
+
*
|
|
68
|
+
* Enabling appends the descriptor to `service_tiers` and "ultrafast" to the deprecated
|
|
69
|
+
* `additional_speed_tiers` mirror, after the existing Fast tier and only when absent, and records
|
|
70
|
+
* each field it filled in the provenance marker. A repeated sync is a no-op and an
|
|
71
|
+
* operator-written Ultrafast entry wins. Disabling removes only what the marker names (and only
|
|
72
|
+
* the unedited generated descriptor), then drops the marker; rows without it are left as they
|
|
73
|
+
* are. The catalog default tier is never set.
|
|
74
|
+
*/
|
|
75
|
+
export function applyNativeUltraFastTier(entry: RawEntry, enabled: boolean): void {
|
|
76
|
+
const owned = generatedFields(entry);
|
|
77
|
+
if (nativeUltraFastSlug(entry) === undefined) {
|
|
78
|
+
delete entry[NATIVE_ULTRAFAST_PROVENANCE_FIELD];
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const tiers = Array.isArray(entry.service_tiers) ? entry.service_tiers : undefined;
|
|
82
|
+
const speeds = Array.isArray(entry.additional_speed_tiers) ? entry.additional_speed_tiers : undefined;
|
|
83
|
+
if (enabled) {
|
|
84
|
+
if (!tiers?.some(isUltraFastDescriptor)) {
|
|
85
|
+
entry.service_tiers = [...(tiers ?? []), { ...NATIVE_ULTRAFAST_SERVICE_TIER }];
|
|
86
|
+
owned.add("service_tiers");
|
|
87
|
+
}
|
|
88
|
+
if (!speeds?.some(isUltraFastId)) {
|
|
89
|
+
entry.additional_speed_tiers = [...(speeds ?? []), NATIVE_ULTRAFAST_SERVICE_TIER.id];
|
|
90
|
+
owned.add("additional_speed_tiers");
|
|
91
|
+
}
|
|
92
|
+
if (owned.size > 0) entry[NATIVE_ULTRAFAST_PROVENANCE_FIELD] = [...owned].sort();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
delete entry[NATIVE_ULTRAFAST_PROVENANCE_FIELD];
|
|
96
|
+
if (owned.has("service_tiers") && tiers) {
|
|
97
|
+
const kept = tiers.filter(tier => !isGeneratedDescriptor(tier));
|
|
98
|
+
if (kept.length > 0) entry.service_tiers = kept;
|
|
99
|
+
else delete entry.service_tiers;
|
|
100
|
+
}
|
|
101
|
+
if (owned.has("additional_speed_tiers") && speeds) {
|
|
102
|
+
const kept = speeds.filter(speed => !isUltraFastId(speed));
|
|
103
|
+
if (kept.length > 0) entry.additional_speed_tiers = kept;
|
|
104
|
+
else delete entry.additional_speed_tiers;
|
|
105
|
+
}
|
|
106
|
+
}
|