@yansigit/opencodex 2.31.3 → 2.33.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 +2 -2
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-DKLr4LTE.js +102 -0
- package/gui/dist/assets/index-DrSQdTRd.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -5
- package/src/adapters/anthropic.ts +25 -13
- package/src/adapters/azure.ts +20 -4
- package/src/adapters/base.ts +5 -1
- package/src/adapters/command-code.ts +42 -10
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/protobuf-events.ts +158 -7
- package/src/adapters/cursor/protobuf-request.ts +33 -15
- package/src/adapters/cursor/request-builder.ts +4 -3
- package/src/adapters/cursor/tool-definitions.ts +27 -1
- package/src/adapters/cursor/types.ts +4 -3
- package/src/adapters/cursor.ts +9 -0
- package/src/adapters/google-antigravity-replay.ts +2 -2
- package/src/adapters/google-antigravity-wire.ts +7 -0
- package/src/adapters/google-errors.ts +6 -2
- package/src/adapters/google-http.ts +30 -7
- package/src/adapters/google-truncation.ts +5 -0
- package/src/adapters/google-wire-compiler.ts +38 -6
- package/src/adapters/google.ts +154 -33
- package/src/adapters/kiro-tools.ts +20 -9
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-chat.ts +9 -0
- package/src/adapters/openai-responses.ts +4 -1
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +133 -24
- package/src/claude/context-windows.ts +16 -9
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +26 -13
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +20 -6
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +8 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +25 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/auth-api.ts +4 -2
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +12 -0
- package/src/codex/catalog/effort.ts +42 -12
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/model-metadata.ts +593 -0
- package/src/codex/catalog/parsing.ts +71 -27
- package/src/codex/catalog/provider-fetch.ts +189 -33
- package/src/codex/catalog/sync.ts +6 -5
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/shim.ts +56 -3
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +214 -0
- package/src/config.ts +153 -814
- package/src/generated/compatibility-version.json +232 -148
- package/src/images/loop.ts +37 -6
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lib/azure-identity.ts +154 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/debug.ts +42 -0
- package/src/lib/errors.ts +14 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/provider-outbound.ts +45 -33
- package/src/lib/provider-tls-profile.ts +309 -0
- package/src/lib/proxy-env.ts +49 -0
- package/src/lib/redact.ts +10 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/antigravity-routing.ts +282 -236
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/command-code.ts +5 -16
- package/src/oauth/google-antigravity.ts +42 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +15 -3
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +6 -0
- package/src/providers/antigravity-quota.ts +3 -1
- package/src/providers/api-keys.ts +2 -1
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/key-failover.ts +5 -1
- package/src/providers/openai-tiers.ts +5 -0
- package/src/providers/provider-id-rewrite.ts +1 -0
- package/src/providers/quota.ts +59 -13
- package/src/providers/registry.ts +4 -2
- package/src/providers/request-pacing.ts +33 -6
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/google-provider-options.ts +36 -0
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +14 -2
- package/src/responses/provider-opaque-metadata.ts +3 -3
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +37 -0
- package/src/responses/state.ts +94 -4
- package/src/router.ts +8 -2
- package/src/server/auth-cors.ts +37 -7
- package/src/server/images.ts +19 -35
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +206 -16
- package/src/server/management/combo-routes.ts +6 -0
- package/src/server/management/config-routes.ts +35 -6
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +25 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +113 -15
- package/src/server/management/routing-profile-routes.ts +3 -0
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/request-log.ts +21 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +36 -22
- package/src/server/responses/core.ts +584 -247
- package/src/server/responses/empty-completion-guard.ts +35 -6
- package/src/server/responses/fetch-helpers.ts +20 -102
- package/src/server/responses/v2-native-parent-override.ts +59 -0
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/service.ts +9 -5
- package/src/types/config.ts +16 -1
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +34 -1
- package/src/types/tools.ts +114 -11
- package/src/types.ts +7 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +6 -4
- package/src/web-search/loop.ts +42 -6
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
- package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
package/src/codex/shim.ts
CHANGED
|
@@ -603,6 +603,47 @@ function backupPathFor(path: string): string {
|
|
|
603
603
|
return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
|
|
604
604
|
}
|
|
605
605
|
|
|
606
|
+
/**
|
|
607
|
+
* True when a Codex binary lives inside a version manager's install tree.
|
|
608
|
+
*
|
|
609
|
+
* These trees are rewritten in place on upgrade, which destroys both the shim
|
|
610
|
+
* and the sibling .opencodex-real backup it restores from (#2412). The tempting
|
|
611
|
+
* repair — adopt the newly installed binary as a fresh original — is wrong
|
|
612
|
+
* twice: it records a provenance that never happened, and the next upgrade wipes
|
|
613
|
+
* it again, so the repair silently un-repairs on the version manager's schedule.
|
|
614
|
+
*
|
|
615
|
+
* Scope is the three managers named in the report. nvm/fnm/npm-prefix are
|
|
616
|
+
* deliberately excluded: a false positive here refuses a restore that would
|
|
617
|
+
* otherwise be correct.
|
|
618
|
+
*/
|
|
619
|
+
export function isVersionManagerOwnedCodexPath(path: string): boolean {
|
|
620
|
+
const normalized = path.replace(/\\/g, "/").toLowerCase();
|
|
621
|
+
return normalized.includes("/mise/installs/")
|
|
622
|
+
|| normalized.includes("/mise/shims/")
|
|
623
|
+
|| normalized.includes("/.asdf/installs/")
|
|
624
|
+
|| normalized.includes("/.asdf/shims/")
|
|
625
|
+
|| normalized.includes("/.volta/");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Why auto-restore refused, in the operator's own terms. Auto-restore used to
|
|
630
|
+
* return a bare `{ status: "ineligible" }`, and the CLI warns only when a
|
|
631
|
+
* message is present, so `ocx start`, `ocx ensure`, and `ocx service repair` all
|
|
632
|
+
* reported success while routing quietly stayed native (#2412, the cause behind
|
|
633
|
+
* the misleading green status in #2411).
|
|
634
|
+
*/
|
|
635
|
+
function destroyedShimMessage(file: ShimFileState): string {
|
|
636
|
+
const wrapper = existsSync(file.wrapperPath)
|
|
637
|
+
? isShim(file.wrapperPath) ? "present but unusable" : "present but not an opencodex shim"
|
|
638
|
+
: "missing";
|
|
639
|
+
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
640
|
+
const base = `Codex autostart shim not restored: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
641
|
+
if (!isVersionManagerOwnedCodexPath(file.wrapperPath)) {
|
|
642
|
+
return `${base} Re-run 'ocx codex-shim install' once the Codex binary is stable.`;
|
|
643
|
+
}
|
|
644
|
+
return `${base} This Codex binary is owned by a version manager (mise/asdf/volta), so opencodex will not wrap it as a new original — the next upgrade would overwrite the shim and its backup again. Route through Codex instead with 'ocx start', and use 'ocx service install' for autostart.`;
|
|
645
|
+
}
|
|
646
|
+
|
|
606
647
|
function shQuote(value: string): string {
|
|
607
648
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
608
649
|
}
|
|
@@ -2039,14 +2080,20 @@ export function autoRestoreCodexShim(options: {
|
|
|
2039
2080
|
if (seen.has(file.wrapperPath)) return { status: "ineligible" };
|
|
2040
2081
|
seen.add(file.wrapperPath);
|
|
2041
2082
|
if (file.preserveOnly) {
|
|
2042
|
-
if (!existsSync(file.backupPath) || existsSync(file.originalPath))
|
|
2083
|
+
if (!existsSync(file.backupPath) || existsSync(file.originalPath)) {
|
|
2084
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2085
|
+
}
|
|
2043
2086
|
continue;
|
|
2044
2087
|
}
|
|
2045
|
-
if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file))
|
|
2088
|
+
if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file)) {
|
|
2089
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2090
|
+
}
|
|
2046
2091
|
const probe = stableShimPathProbe(file.wrapperPath);
|
|
2047
2092
|
if (!probe) return { status: "deferred" };
|
|
2048
2093
|
if (probe.prefix.includes(SHIM_MARKER)) {
|
|
2049
|
-
if (!isHealthyShimProbe(probe, state.platform))
|
|
2094
|
+
if (!isHealthyShimProbe(probe, state.platform)) {
|
|
2095
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2096
|
+
}
|
|
2050
2097
|
if (state.platform !== "win32" && !isCurrentUnixShimProbe(probe)) {
|
|
2051
2098
|
obsoleteShimProbes.set(file.wrapperPath, probe);
|
|
2052
2099
|
continue;
|
|
@@ -2054,6 +2101,12 @@ export function autoRestoreCodexShim(options: {
|
|
|
2054
2101
|
healthyCount += 1;
|
|
2055
2102
|
continue;
|
|
2056
2103
|
}
|
|
2104
|
+
// A surviving backup would otherwise let the replacement path below wrap the
|
|
2105
|
+
// version manager's NEW binary as a fresh original — the same adoption the
|
|
2106
|
+
// missing-backup case refuses, arriving through the back door.
|
|
2107
|
+
if (isVersionManagerOwnedCodexPath(file.wrapperPath)) {
|
|
2108
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2109
|
+
}
|
|
2057
2110
|
replacementProbes.set(file.wrapperPath, probe);
|
|
2058
2111
|
}
|
|
2059
2112
|
|
package/src/combos/failover.ts
CHANGED
|
@@ -109,6 +109,28 @@ export function clearComboTargetCooldowns(comboId?: string): void {
|
|
|
109
109
|
|
|
110
110
|
export type ComboFailureDecision = "hop" | "stop";
|
|
111
111
|
|
|
112
|
+
function isModelLifecycleGone(
|
|
113
|
+
status: number,
|
|
114
|
+
message: string,
|
|
115
|
+
code?: string | null,
|
|
116
|
+
): boolean {
|
|
117
|
+
if (status !== 410) return false;
|
|
118
|
+
const normalizedCode = code?.trim().toLowerCase().replaceAll("-", "_");
|
|
119
|
+
if ([
|
|
120
|
+
"model_deprecated",
|
|
121
|
+
"model_end_of_life",
|
|
122
|
+
"model_eol",
|
|
123
|
+
"model_not_found",
|
|
124
|
+
"model_retired",
|
|
125
|
+
].includes(normalizedCode ?? "")) return true;
|
|
126
|
+
const text = message.toLowerCase();
|
|
127
|
+
return /\bmodel\b/.test(text) && (
|
|
128
|
+
/\bend[ -]of[ -]life\b/.test(text)
|
|
129
|
+
|| /\bno longer available\b/.test(text)
|
|
130
|
+
|| /\b(?:deprecated|retired|retirement|sunset|decommissioned)\b/.test(text)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
export function comboFailureDecision(
|
|
113
135
|
status: number,
|
|
114
136
|
message: string,
|
|
@@ -119,6 +141,11 @@ export function comboFailureDecision(
|
|
|
119
141
|
// Cyber policy is a hard non-retryable refusal — honor structured code even when
|
|
120
142
|
// classificationText was truncated before the JSON code field.
|
|
121
143
|
if (isCyberPolicyCode(options?.code)) return "stop";
|
|
144
|
+
// HTTP 410 is normally terminal. A model-specific lifecycle verdict is target-local,
|
|
145
|
+
// however: another provider/model in the declared combo can still serve the request.
|
|
146
|
+
// Require structured lifecycle code or explicit model+lifecycle prose so unrelated
|
|
147
|
+
// application-level 410 responses remain fail-closed.
|
|
148
|
+
if (isModelLifecycleGone(status, message, options?.code)) return "hop";
|
|
122
149
|
const error = classifyError(status, "upstream_error", message);
|
|
123
150
|
if (isCyberPolicyCode(error.code)) return "stop";
|
|
124
151
|
// A local input-admission refusal (#1524) says "this candidate cannot fit the request",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST } from "./openai-responses";
|
|
2
|
+
import type { CompatibilityManifestV1 } from "./manifest";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
COMPATIBILITY_DISPOSITIONS,
|
|
6
|
+
COMPATIBILITY_MANIFEST_SCHEMA_VERSION,
|
|
7
|
+
compatibilityManifestIssues,
|
|
8
|
+
defineCompatibilityManifest,
|
|
9
|
+
} from "./manifest";
|
|
10
|
+
export type {
|
|
11
|
+
CompatibilityClaimV1,
|
|
12
|
+
CompatibilityDisposition,
|
|
13
|
+
CompatibilityEvidenceKind,
|
|
14
|
+
CompatibilityEvidenceRefV1,
|
|
15
|
+
CompatibilityManifestV1,
|
|
16
|
+
CompatibilitySubjectV1,
|
|
17
|
+
} from "./manifest";
|
|
18
|
+
export { OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST } from "./openai-responses";
|
|
19
|
+
|
|
20
|
+
export const COMPATIBILITY_MANIFESTS: readonly CompatibilityManifestV1[] = Object.freeze([
|
|
21
|
+
OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function getCompatibilityManifest(id: string): CompatibilityManifestV1 | undefined {
|
|
25
|
+
return COMPATIBILITY_MANIFESTS.find(manifest => manifest.id === id);
|
|
26
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
export const COMPATIBILITY_MANIFEST_SCHEMA_VERSION = 1 as const;
|
|
2
|
+
|
|
3
|
+
export const COMPATIBILITY_DISPOSITIONS = [
|
|
4
|
+
"passthrough",
|
|
5
|
+
"translated",
|
|
6
|
+
"degraded",
|
|
7
|
+
"unsupported",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export type CompatibilityDisposition = typeof COMPATIBILITY_DISPOSITIONS[number];
|
|
11
|
+
export type CompatibilityEvidenceKind = "fixture" | "lab-scenario";
|
|
12
|
+
|
|
13
|
+
export interface CompatibilityEvidenceRefV1 {
|
|
14
|
+
kind: CompatibilityEvidenceKind;
|
|
15
|
+
id: string;
|
|
16
|
+
/** Exact fixture assertions that prove this claim. Required for fixture evidence. */
|
|
17
|
+
assertionIds?: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CompatibilityClaimV1 {
|
|
21
|
+
/** Stable manifest-local claim id. */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Stable public feature key, such as `request.previous_response_id`. */
|
|
24
|
+
feature: string;
|
|
25
|
+
disposition: CompatibilityDisposition;
|
|
26
|
+
summary: string;
|
|
27
|
+
/** Required when behavior is not byte/semantic passthrough. */
|
|
28
|
+
limitation?: string;
|
|
29
|
+
evidence: readonly CompatibilityEvidenceRefV1[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CompatibilitySubjectV1 {
|
|
33
|
+
providerId: string;
|
|
34
|
+
/** Exact normalized provider base URL; destination changes require a new subject/version. */
|
|
35
|
+
baseUrl: string;
|
|
36
|
+
adapterId: string;
|
|
37
|
+
authMode: "forward" | "key" | "oauth" | "local";
|
|
38
|
+
inboundProtocol: "responses" | "chat" | "messages";
|
|
39
|
+
upstreamProtocol: string;
|
|
40
|
+
/** Claims apply only to these exact wire model ids. */
|
|
41
|
+
modelIds: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CompatibilityManifestV1 {
|
|
45
|
+
schemaVersion: typeof COMPATIBILITY_MANIFEST_SCHEMA_VERSION;
|
|
46
|
+
id: string;
|
|
47
|
+
version: string;
|
|
48
|
+
subject: CompatibilitySubjectV1;
|
|
49
|
+
claims: readonly CompatibilityClaimV1[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
53
|
+
const FEATURE_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)+$/;
|
|
54
|
+
const DISPOSITIONS = new Set<string>(COMPATIBILITY_DISPOSITIONS);
|
|
55
|
+
const EVIDENCE_KINDS = new Set<string>(["fixture", "lab-scenario"]);
|
|
56
|
+
|
|
57
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
58
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function checkKnownKeys(
|
|
62
|
+
value: Record<string, unknown>,
|
|
63
|
+
allowed: readonly string[],
|
|
64
|
+
path: string,
|
|
65
|
+
issues: string[],
|
|
66
|
+
): void {
|
|
67
|
+
const allowedSet = new Set(allowed);
|
|
68
|
+
for (const key of Object.keys(value)) {
|
|
69
|
+
if (!allowedSet.has(key)) issues.push(`${path}${path ? "." : ""}${key} is not allowed by schema v1`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function checkId(value: unknown, path: string, issues: string[]): value is string {
|
|
74
|
+
if (typeof value !== "string" || !ID_PATTERN.test(value)) {
|
|
75
|
+
issues.push(`${path} must match ${ID_PATTERN}`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function checkNonBlank(value: unknown, path: string, issues: string[]): value is string {
|
|
82
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
83
|
+
issues.push(`${path} must be a non-blank string`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function checkNormalizedBaseUrl(value: unknown, path: string, issues: string[]): value is string {
|
|
90
|
+
if (typeof value !== "string") {
|
|
91
|
+
issues.push(`${path} must be a normalized absolute HTTP(S) base URL`);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const parsed = new URL(value);
|
|
96
|
+
if (
|
|
97
|
+
(parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
98
|
+
|| parsed.username
|
|
99
|
+
|| parsed.password
|
|
100
|
+
|| parsed.search
|
|
101
|
+
|| parsed.hash
|
|
102
|
+
) {
|
|
103
|
+
issues.push(`${path} must be a normalized absolute HTTP(S) base URL without credentials, query, or fragment`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const normalizedPath = parsed.pathname.replace(/\/+$/, "");
|
|
107
|
+
const normalized = `${parsed.origin}${normalizedPath}`;
|
|
108
|
+
if (value !== normalized) {
|
|
109
|
+
issues.push(`${path} must be normalized as ${normalized}`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
issues.push(`${path} must be a normalized absolute HTTP(S) base URL`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function checkSortedUniqueStrings(
|
|
120
|
+
value: unknown,
|
|
121
|
+
path: string,
|
|
122
|
+
issues: string[],
|
|
123
|
+
options: { nonEmpty?: boolean } = {},
|
|
124
|
+
): value is string[] {
|
|
125
|
+
if (!Array.isArray(value) || (options.nonEmpty === true && value.length === 0)) {
|
|
126
|
+
issues.push(`${path} must be${options.nonEmpty === true ? " a non-empty" : " an"} array`);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (value.some(item => typeof item !== "string" || item.trim().length === 0)) {
|
|
130
|
+
issues.push(`${path} must contain only non-blank strings`);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const strings = value as string[];
|
|
134
|
+
if (new Set(strings).size !== strings.length) issues.push(`${path} must not contain duplicates`);
|
|
135
|
+
const sorted = [...strings].sort();
|
|
136
|
+
if (strings.some((item, index) => item !== sorted[index])) issues.push(`${path} must be sorted`);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function checkEvidence(value: unknown, path: string, issues: string[]): void {
|
|
141
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
142
|
+
issues.push(`${path} must be a non-empty array`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const seen = new Set<string>();
|
|
146
|
+
value.forEach((raw, index) => {
|
|
147
|
+
const evidencePath = `${path}[${index}]`;
|
|
148
|
+
if (!isRecord(raw)) {
|
|
149
|
+
issues.push(`${evidencePath} must be an object`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
checkKnownKeys(raw, ["kind", "id", "assertionIds"], evidencePath, issues);
|
|
153
|
+
if (!EVIDENCE_KINDS.has(String(raw.kind))) {
|
|
154
|
+
issues.push(`${evidencePath}.kind must be fixture or lab-scenario`);
|
|
155
|
+
}
|
|
156
|
+
const id = checkId(raw.id, `${evidencePath}.id`, issues) ? raw.id : "";
|
|
157
|
+
const assertionIds = raw.assertionIds;
|
|
158
|
+
if (raw.kind === "fixture") {
|
|
159
|
+
checkSortedUniqueStrings(assertionIds, `${evidencePath}.assertionIds`, issues, { nonEmpty: true });
|
|
160
|
+
} else if (assertionIds !== undefined) {
|
|
161
|
+
checkSortedUniqueStrings(assertionIds, `${evidencePath}.assertionIds`, issues, { nonEmpty: true });
|
|
162
|
+
}
|
|
163
|
+
const key = `${String(raw.kind)}:${id}:${JSON.stringify(assertionIds ?? [])}`;
|
|
164
|
+
if (seen.has(key)) issues.push(`${evidencePath} duplicates an earlier evidence reference`);
|
|
165
|
+
seen.add(key);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate one versioned compatibility manifest without importing provider or Lab state.
|
|
171
|
+
* Keeping this as a leaf lets future CLI/GUI readers load contracts without activating Lab.
|
|
172
|
+
*/
|
|
173
|
+
export function compatibilityManifestIssues(value: unknown): string[] {
|
|
174
|
+
const issues: string[] = [];
|
|
175
|
+
if (!isRecord(value)) return ["manifest must be an object"];
|
|
176
|
+
checkKnownKeys(value, ["schemaVersion", "id", "version", "subject", "claims"], "", issues);
|
|
177
|
+
if (value.schemaVersion !== COMPATIBILITY_MANIFEST_SCHEMA_VERSION) {
|
|
178
|
+
issues.push(`schemaVersion must be ${COMPATIBILITY_MANIFEST_SCHEMA_VERSION}`);
|
|
179
|
+
}
|
|
180
|
+
checkId(value.id, "id", issues);
|
|
181
|
+
checkNonBlank(value.version, "version", issues);
|
|
182
|
+
|
|
183
|
+
if (!isRecord(value.subject)) {
|
|
184
|
+
issues.push("subject must be an object");
|
|
185
|
+
} else {
|
|
186
|
+
checkKnownKeys(
|
|
187
|
+
value.subject,
|
|
188
|
+
["providerId", "baseUrl", "adapterId", "authMode", "inboundProtocol", "upstreamProtocol", "modelIds"],
|
|
189
|
+
"subject",
|
|
190
|
+
issues,
|
|
191
|
+
);
|
|
192
|
+
checkId(value.subject.providerId, "subject.providerId", issues);
|
|
193
|
+
checkNormalizedBaseUrl(value.subject.baseUrl, "subject.baseUrl", issues);
|
|
194
|
+
checkId(value.subject.adapterId, "subject.adapterId", issues);
|
|
195
|
+
if (!["forward", "key", "oauth", "local"].includes(String(value.subject.authMode))) {
|
|
196
|
+
issues.push("subject.authMode must be forward, key, oauth, or local");
|
|
197
|
+
}
|
|
198
|
+
if (!["responses", "chat", "messages"].includes(String(value.subject.inboundProtocol))) {
|
|
199
|
+
issues.push("subject.inboundProtocol must be responses, chat, or messages");
|
|
200
|
+
}
|
|
201
|
+
checkNonBlank(value.subject.upstreamProtocol, "subject.upstreamProtocol", issues);
|
|
202
|
+
checkSortedUniqueStrings(value.subject.modelIds, "subject.modelIds", issues, { nonEmpty: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!Array.isArray(value.claims) || value.claims.length === 0) {
|
|
206
|
+
issues.push("claims must be a non-empty array");
|
|
207
|
+
return issues;
|
|
208
|
+
}
|
|
209
|
+
const claimIds = new Set<string>();
|
|
210
|
+
const claimFeatures = new Set<string>();
|
|
211
|
+
value.claims.forEach((raw, index) => {
|
|
212
|
+
const path = `claims[${index}]`;
|
|
213
|
+
if (!isRecord(raw)) {
|
|
214
|
+
issues.push(`${path} must be an object`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
checkKnownKeys(raw, ["id", "feature", "disposition", "summary", "limitation", "evidence"], path, issues);
|
|
218
|
+
const id = checkId(raw.id, `${path}.id`, issues) ? raw.id : "";
|
|
219
|
+
if (id && claimIds.has(id)) issues.push(`${path}.id duplicates ${id}`);
|
|
220
|
+
if (id) claimIds.add(id);
|
|
221
|
+
if (typeof raw.feature !== "string" || !FEATURE_PATTERN.test(raw.feature)) {
|
|
222
|
+
issues.push(`${path}.feature must be a dotted stable feature key`);
|
|
223
|
+
} else if (claimFeatures.has(raw.feature)) {
|
|
224
|
+
issues.push(`${path}.feature duplicates ${raw.feature}`);
|
|
225
|
+
} else {
|
|
226
|
+
claimFeatures.add(raw.feature);
|
|
227
|
+
}
|
|
228
|
+
if (!DISPOSITIONS.has(String(raw.disposition))) {
|
|
229
|
+
issues.push(`${path}.disposition is invalid`);
|
|
230
|
+
}
|
|
231
|
+
checkNonBlank(raw.summary, `${path}.summary`, issues);
|
|
232
|
+
if (raw.disposition !== "passthrough") {
|
|
233
|
+
checkNonBlank(raw.limitation, `${path}.limitation`, issues);
|
|
234
|
+
} else if (raw.limitation !== undefined) {
|
|
235
|
+
issues.push(`${path}.limitation must be omitted for passthrough claims`);
|
|
236
|
+
}
|
|
237
|
+
checkEvidence(raw.evidence, `${path}.evidence`, issues);
|
|
238
|
+
});
|
|
239
|
+
return issues;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function deepFreeze<T>(value: T, seen = new WeakSet<object>()): Readonly<T> {
|
|
243
|
+
if (!value || typeof value !== "object" || seen.has(value)) return value;
|
|
244
|
+
seen.add(value);
|
|
245
|
+
for (const nested of Object.values(value as Record<string, unknown>)) deepFreeze(nested, seen);
|
|
246
|
+
return Object.freeze(value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function defineCompatibilityManifest<T extends CompatibilityManifestV1>(manifest: T): Readonly<T> {
|
|
250
|
+
const issues = compatibilityManifestIssues(manifest);
|
|
251
|
+
if (issues.length > 0) throw new TypeError(`Invalid compatibility manifest ${manifest.id}: ${issues.join("; ")}`);
|
|
252
|
+
return deepFreeze(manifest);
|
|
253
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { defineCompatibilityManifest } from "./manifest";
|
|
2
|
+
|
|
3
|
+
const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* First deliberately narrow compatibility slice: one exact native Codex-login model.
|
|
7
|
+
* Broader model/provider claims require their own fixtures instead of inheriting this row.
|
|
8
|
+
*/
|
|
9
|
+
export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({
|
|
10
|
+
schemaVersion: 1,
|
|
11
|
+
id: "openai.codex-forward.gpt-5-6-sol.responses",
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
subject: {
|
|
14
|
+
providerId: "openai",
|
|
15
|
+
baseUrl: "https://chatgpt.com/backend-api/codex",
|
|
16
|
+
adapterId: "openai-responses",
|
|
17
|
+
authMode: "forward",
|
|
18
|
+
inboundProtocol: "responses",
|
|
19
|
+
upstreamProtocol: "openai-responses",
|
|
20
|
+
modelIds: ["gpt-5.6-sol"],
|
|
21
|
+
},
|
|
22
|
+
claims: [
|
|
23
|
+
{
|
|
24
|
+
id: "custom-tools",
|
|
25
|
+
feature: "request.custom_tools",
|
|
26
|
+
disposition: "passthrough",
|
|
27
|
+
summary: "Native custom-tool declarations retain their Responses representation.",
|
|
28
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["custom-tool-preserved"] }],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "prompt-cache-key",
|
|
32
|
+
feature: "request.prompt_cache_key",
|
|
33
|
+
disposition: "passthrough",
|
|
34
|
+
summary: "The caller's prompt cache key is forwarded unchanged.",
|
|
35
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-key-preserved"] }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "previous-response-id",
|
|
39
|
+
feature: "request.previous_response_id",
|
|
40
|
+
disposition: "translated",
|
|
41
|
+
summary: "Continuation is owned by the proxy rather than the ChatGPT backend field.",
|
|
42
|
+
limitation: "OpenCodex expands remembered input locally and removes previous_response_id before dispatch.",
|
|
43
|
+
evidence: [
|
|
44
|
+
{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["previous-response-id-removed"] },
|
|
45
|
+
{ kind: "lab-scenario", id: "codex-core.protocol.previous-response-replay" },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "orphan-tool-output",
|
|
50
|
+
feature: "continuation.orphan_tool_output",
|
|
51
|
+
disposition: "degraded",
|
|
52
|
+
summary: "An output whose prior call is unavailable remains visible to the model.",
|
|
53
|
+
limitation: "The orphan output is converted to user text because the forward backend cannot accept the unmatched item.",
|
|
54
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["orphan-output-converted"] }],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "max-output-tokens",
|
|
58
|
+
feature: "request.max_output_tokens",
|
|
59
|
+
disposition: "unsupported",
|
|
60
|
+
summary: "The ChatGPT Codex forward route does not receive max_output_tokens.",
|
|
61
|
+
limitation: "The field is removed before dispatch to avoid a backend parameter rejection.",
|
|
62
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["max-output-tokens-removed"] }],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "metadata",
|
|
66
|
+
feature: "request.metadata",
|
|
67
|
+
disposition: "unsupported",
|
|
68
|
+
summary: "Caller metadata is not forwarded to the ChatGPT Codex backend.",
|
|
69
|
+
limitation: "The field is removed before dispatch.",
|
|
70
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["metadata-removed"] }],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "prompt-cache-retention",
|
|
74
|
+
feature: "request.prompt_cache_retention",
|
|
75
|
+
disposition: "unsupported",
|
|
76
|
+
summary: "The retired retention field is not sent for gpt-5.6-sol.",
|
|
77
|
+
limitation: "The field is removed without inventing a replacement prompt_cache_options value.",
|
|
78
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-retention-removed"] }],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
} as const);
|