@bitkyc08/opencodex 2.28.0 → 2.29.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 +9 -1
- package/gui/dist/assets/index-BNESwCzn.js +102 -0
- package/gui/dist/assets/index-CH7ncHCC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +3 -1
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/discovery.ts +25 -0
- package/src/adapters/cursor/live-transport.ts +19 -2
- package/src/adapters/cursor/native-exec.ts +47 -2
- package/src/adapters/cursor/protobuf-request.ts +80 -23
- package/src/adapters/cursor/request-builder.ts +119 -5
- package/src/adapters/cursor/transport.ts +5 -0
- package/src/adapters/cursor/types.ts +13 -0
- package/src/adapters/cursor.ts +109 -3
- package/src/adapters/google-antigravity-replay.ts +31 -4
- package/src/adapters/google.ts +323 -38
- package/src/adapters/openai-chat.ts +19 -0
- package/src/adapters/openai-responses.ts +218 -37
- package/src/bridge.ts +38 -20
- package/src/claude/desktop-3p.ts +15 -6
- package/src/cli/agent.ts +44 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/dispatch.ts +24 -1
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +40 -38
- package/src/cli/integrations.ts +1 -1
- package/src/cli/registry.ts +2 -2
- package/src/clients/config-export.ts +119 -20
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/inject.ts +46 -14
- package/src/codex/journal.ts +22 -8
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +134 -66
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/registry.ts +25 -2
- package/src/integrations/writer.ts +32 -1
- package/src/lab/public/signature.ts +25 -1
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/redact.ts +2 -2
- package/src/oauth/log.ts +3 -1
- package/src/providers/derive.ts +9 -0
- package/src/providers/fastwire.ts +24 -18
- package/src/providers/openai-tiers.ts +60 -1
- package/src/providers/registry.ts +14 -7
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/responses/compaction.ts +18 -0
- package/src/responses/custom-tool-compat.ts +70 -5
- package/src/responses/namespace-tool-compat.ts +356 -0
- package/src/responses/parser.ts +2 -2
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/reasoning-replay-cache.ts +125 -7
- package/src/responses/spill-store.ts +6 -1
- package/src/responses/state.ts +11 -0
- package/src/router.ts +14 -0
- package/src/routing/compatibility/behavior.ts +1 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +57 -9
- package/src/server/management/config-routes.ts +134 -15
- package/src/server/management/integration-routes.ts +8 -55
- package/src/server/management/model-routes.ts +23 -1
- package/src/server/management/provider-routes.ts +22 -0
- package/src/server/management/vision-sidecar-options.ts +18 -11
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/responses/core.ts +588 -78
- package/src/server/responses/responses-field-backfill.ts +7 -11
- package/src/server/responses/terminal-guard.ts +22 -11
- package/src/server/responses-custom-tool-repair.ts +3 -1
- package/src/server/responses-reasoning-summary-rewrite.ts +7 -0
- package/src/server/responses-tool-search-repair.ts +64 -14
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/types/config.ts +26 -5
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +26 -0
- package/src/types.ts +1 -0
- package/src/usage/log.ts +2 -0
- package/src/vision/index.ts +8 -9
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +139 -12
- package/src/web-search/loop.ts +45 -5
- package/src/web-search/parse.ts +34 -23
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-D2sP-biU.js +0 -102
- package/gui/dist/assets/index-DQsMZzI5.css +0 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { IntegrationClientId } from "./registry";
|
|
2
|
+
|
|
3
|
+
const INTEGRATION_MUTATION_JOIN_MS = 120_000;
|
|
4
|
+
export const INTEGRATION_MUTATION_TERMINAL_MS = 10 * 60_000;
|
|
5
|
+
|
|
6
|
+
interface IntegrationMutationFlight {
|
|
7
|
+
key: string;
|
|
8
|
+
startedAt: number;
|
|
9
|
+
promise: Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class IntegrationMutationBusyError extends Error {
|
|
13
|
+
constructor(readonly clientId: IntegrationClientId) {
|
|
14
|
+
super("integration_mutation_busy");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const integrationMutationFlights = new Map<IntegrationClientId, IntegrationMutationFlight>();
|
|
19
|
+
let integrationMutationRunTestHook: ((operation: () => Promise<unknown>) => Promise<unknown>) | null = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One in-process flight per client, shared by explicit HTTP mutations and
|
|
23
|
+
* implicit catalog refreshes. Equal operations join; different semantics are
|
|
24
|
+
* busy instead of, for example, letting an explicit apply join a refresh-only
|
|
25
|
+
* no-op for an absent block.
|
|
26
|
+
*/
|
|
27
|
+
export function runIntegrationMutationFlight<T>(
|
|
28
|
+
clientId: IntegrationClientId,
|
|
29
|
+
key: string,
|
|
30
|
+
now: () => number,
|
|
31
|
+
operation: () => Promise<T>,
|
|
32
|
+
): Promise<T> {
|
|
33
|
+
const startedAt = now();
|
|
34
|
+
const current = integrationMutationFlights.get(clientId);
|
|
35
|
+
if (current) {
|
|
36
|
+
const age = startedAt - current.startedAt;
|
|
37
|
+
if (current.key === key && age < INTEGRATION_MUTATION_JOIN_MS) {
|
|
38
|
+
return current.promise as Promise<T>;
|
|
39
|
+
}
|
|
40
|
+
if (age <= INTEGRATION_MUTATION_TERMINAL_MS) {
|
|
41
|
+
return Promise.reject(new IntegrationMutationBusyError(clientId));
|
|
42
|
+
}
|
|
43
|
+
if (integrationMutationFlights.get(clientId) === current) {
|
|
44
|
+
integrationMutationFlights.delete(clientId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const flight: IntegrationMutationFlight = {
|
|
49
|
+
key,
|
|
50
|
+
startedAt,
|
|
51
|
+
promise: Promise.resolve(),
|
|
52
|
+
};
|
|
53
|
+
const run = async (): Promise<unknown> => operation();
|
|
54
|
+
flight.promise = (integrationMutationRunTestHook
|
|
55
|
+
? integrationMutationRunTestHook(run)
|
|
56
|
+
: run()
|
|
57
|
+
).finally(() => {
|
|
58
|
+
if (integrationMutationFlights.get(clientId) === flight) {
|
|
59
|
+
integrationMutationFlights.delete(clientId);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
integrationMutationFlights.set(clientId, flight);
|
|
63
|
+
return flight.promise as Promise<T>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function setIntegrationMutationFlightTestHook(
|
|
67
|
+
run: ((operation: () => Promise<unknown>) => Promise<unknown>) | null,
|
|
68
|
+
): void {
|
|
69
|
+
integrationMutationRunTestHook = run;
|
|
70
|
+
integrationMutationFlights.clear();
|
|
71
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refresh a file integration only when OpenCodex already owns its block.
|
|
3
|
+
*
|
|
4
|
+
* This is the safe bridge between an implicit operation such as `ocx sync`
|
|
5
|
+
* and the explicit integration writer. An implicit sync may update a block
|
|
6
|
+
* the user previously enabled, but it must never claim an unowned block,
|
|
7
|
+
* recreate one the user removed, or overwrite edits made after our write.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExportModel } from "../clients/config-export";
|
|
10
|
+
import type { OcxConfig } from "../types";
|
|
11
|
+
import type { IntegrationIO } from "./config-io";
|
|
12
|
+
import type { IntegrationClientId } from "./registry";
|
|
13
|
+
import { runIntegrationMutationFlight } from "./mutation-flight";
|
|
14
|
+
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
15
|
+
import {
|
|
16
|
+
refreshIntegrationCoordinated,
|
|
17
|
+
type CoordinatedIntegrationOptions,
|
|
18
|
+
} from "./writer";
|
|
19
|
+
|
|
20
|
+
export interface OwnedIntegrationRefreshInput {
|
|
21
|
+
clientId: IntegrationClientId;
|
|
22
|
+
/** Lazy in `ocx sync`, so an unowned client does not even load the catalog. */
|
|
23
|
+
models: readonly ExportModel[] | (() => Promise<readonly ExportModel[]>);
|
|
24
|
+
config: OcxConfig;
|
|
25
|
+
port: number;
|
|
26
|
+
env?: NodeJS.ProcessEnv;
|
|
27
|
+
home?: string;
|
|
28
|
+
store?: IntegrationStateStore;
|
|
29
|
+
io?: IntegrationIO;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OwnedIntegrationRefreshOutcome {
|
|
33
|
+
readonly client: IntegrationClientId;
|
|
34
|
+
readonly ok: boolean;
|
|
35
|
+
readonly changed?: boolean;
|
|
36
|
+
readonly reason?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns `null` when the client has never been connected by OpenCodex.
|
|
41
|
+
* Callers use that distinction to report "left alone" rather than "skipped".
|
|
42
|
+
*/
|
|
43
|
+
export async function refreshOwnedIntegration(
|
|
44
|
+
input: OwnedIntegrationRefreshInput,
|
|
45
|
+
options?: CoordinatedIntegrationOptions,
|
|
46
|
+
): Promise<OwnedIntegrationRefreshOutcome | null> {
|
|
47
|
+
const store = input.store ?? createIntegrationStateStore();
|
|
48
|
+
|
|
49
|
+
// The ownership record is the user's durable opt-in. Do this check before
|
|
50
|
+
// reading or classifying the target so an implicit sync can never turn an
|
|
51
|
+
// unowned provider block into one of ours.
|
|
52
|
+
const record = store.readRecords()[input.clientId];
|
|
53
|
+
if (!record || record.clientId !== input.clientId) return null;
|
|
54
|
+
|
|
55
|
+
const { models: suppliedModels, ...rest } = input;
|
|
56
|
+
const models = typeof suppliedModels === "function"
|
|
57
|
+
? await suppliedModels()
|
|
58
|
+
: suppliedModels;
|
|
59
|
+
const bound = { ...rest, models, store };
|
|
60
|
+
const result = await runIntegrationMutationFlight(
|
|
61
|
+
input.clientId,
|
|
62
|
+
"refresh",
|
|
63
|
+
input.io?.now ?? Date.now,
|
|
64
|
+
() => refreshIntegrationCoordinated(bound, options),
|
|
65
|
+
);
|
|
66
|
+
return result.ok
|
|
67
|
+
? {
|
|
68
|
+
client: input.clientId,
|
|
69
|
+
ok: true,
|
|
70
|
+
changed: result.changed,
|
|
71
|
+
...(result.state === "absent" ? { reason: result.message } : {}),
|
|
72
|
+
}
|
|
73
|
+
: { client: input.clientId, ok: false, reason: result.message };
|
|
74
|
+
}
|
|
@@ -27,6 +27,10 @@ import {
|
|
|
27
27
|
opencodeGlobalConfigPath,
|
|
28
28
|
openclawConfigPath,
|
|
29
29
|
openclawHomeDir,
|
|
30
|
+
piAgentDir,
|
|
31
|
+
piConfigPath,
|
|
32
|
+
primeAgentDir,
|
|
33
|
+
primeConfigPath,
|
|
30
34
|
zcodeConfigPath,
|
|
31
35
|
zcodeHomeDir,
|
|
32
36
|
type ExportClientId,
|
|
@@ -77,8 +81,18 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
77
81
|
},
|
|
78
82
|
pi: {
|
|
79
83
|
id: "pi",
|
|
80
|
-
configPath: (
|
|
81
|
-
|
|
84
|
+
configPath: (env = process.env, home = homedir()) => piConfigPath(env, home),
|
|
85
|
+
/*
|
|
86
|
+
* Deliberately not `piAgentDir` unconditionally. Without an override the
|
|
87
|
+
* install signal stays `~/.pi`, which is what it has always been: narrowing
|
|
88
|
+
* it to `~/.pi/agent` would flip a user who has the former without the
|
|
89
|
+
* latter from installed to absent, and this change is about honoring the
|
|
90
|
+
* override, not about redefining detection. With an override there is no
|
|
91
|
+
* parent worth testing — the variable names the agent directory itself — so
|
|
92
|
+
* that directory becomes the signal.
|
|
93
|
+
*/
|
|
94
|
+
detectDir: (env = process.env, home = homedir()) =>
|
|
95
|
+
env.PI_CODING_AGENT_DIR?.trim() ? piAgentDir(env, home) : join(home, ".pi"),
|
|
82
96
|
},
|
|
83
97
|
omp: {
|
|
84
98
|
id: "omp",
|
|
@@ -120,12 +134,21 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
120
134
|
id: "mcode",
|
|
121
135
|
configPath: (env = process.env, home = homedir()) => mcodeConfigPath(env, home),
|
|
122
136
|
detectDir: (env = process.env, home = homedir()) => mcodeHomeDir(env, home),
|
|
137
|
+
writerLock: { suffix: ".lock" },
|
|
123
138
|
},
|
|
124
139
|
zcode: {
|
|
125
140
|
id: "zcode",
|
|
126
141
|
configPath: (env = process.env, home = homedir()) => zcodeConfigPath(env, home),
|
|
127
142
|
detectDir: (env = process.env, home = homedir()) => zcodeHomeDir(env, home),
|
|
128
143
|
},
|
|
144
|
+
prime: {
|
|
145
|
+
id: "prime",
|
|
146
|
+
configPath: (env = process.env, home = homedir()) => primeConfigPath(env, home),
|
|
147
|
+
// The agent directory, not its parent: `PRIME_AGENT_CODING_AGENT_DIR` names
|
|
148
|
+
// that directory directly, so there is no parent to test when the override
|
|
149
|
+
// is set. Same choice as OMP, whose detect signal is `ompAgentDir`.
|
|
150
|
+
detectDir: (env = process.env, home = homedir()) => primeAgentDir(env, home),
|
|
151
|
+
},
|
|
129
152
|
};
|
|
130
153
|
|
|
131
154
|
export const INTEGRATION_CLIENT_IDS: readonly IntegrationClientId[] =
|
|
@@ -240,7 +240,7 @@ function preflight(input: IntegrationWriteInput) {
|
|
|
240
240
|
return { failed: undefined, store, io, clientId, spec, exportSpec, configPath, before, parsed, contribution, record, classified } as const;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
function applyOrRefreshIntegration(input: IntegrationWriteInput, allowAbsent: boolean): WriteOutcome {
|
|
244
244
|
const pre = preflight(input);
|
|
245
245
|
if (pre.failed) return pre.failed;
|
|
246
246
|
const { store, io, clientId, spec, exportSpec, configPath, before, parsed, contribution, record, classified } = pre;
|
|
@@ -271,6 +271,21 @@ export function applyIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
271
271
|
? `${configPath} holds a value where opencodex would have to write a section, so applying would replace it`
|
|
272
272
|
: `${configPath} cannot be changed safely`);
|
|
273
273
|
}
|
|
274
|
+
/*
|
|
275
|
+
* An implicit catalog sync is refresh-only. Keeping this decision inside the
|
|
276
|
+
* writer's one preflight closes the read-then-apply race where a user could
|
|
277
|
+
* remove the managed block after a caller classified it as stale and a
|
|
278
|
+
* normal apply would silently recreate it.
|
|
279
|
+
*/
|
|
280
|
+
if (classified.state === "absent" && !allowAbsent) {
|
|
281
|
+
return {
|
|
282
|
+
ok: true,
|
|
283
|
+
changed: false,
|
|
284
|
+
state: "absent",
|
|
285
|
+
clientId,
|
|
286
|
+
message: "managed block is absent; refresh did not reconnect it",
|
|
287
|
+
};
|
|
288
|
+
}
|
|
274
289
|
if (classified.state === "current") {
|
|
275
290
|
return { ok: true, changed: false, state: "current", clientId, message: "already applied" };
|
|
276
291
|
}
|
|
@@ -352,6 +367,15 @@ export function applyIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
352
367
|
});
|
|
353
368
|
}
|
|
354
369
|
|
|
370
|
+
export function applyIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
371
|
+
return applyOrRefreshIntegration(input, true);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Refresh an owned stale block, but never create or reconnect an absent one. */
|
|
375
|
+
export function refreshIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
376
|
+
return applyOrRefreshIntegration(input, false);
|
|
377
|
+
}
|
|
378
|
+
|
|
355
379
|
export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
356
380
|
const pre = preflight(input);
|
|
357
381
|
if (pre.failed) return pre.failed;
|
|
@@ -627,6 +651,13 @@ export function applyIntegrationCoordinated(
|
|
|
627
651
|
return coordinatedWrite(input, applyIntegration, options);
|
|
628
652
|
}
|
|
629
653
|
|
|
654
|
+
export function refreshIntegrationCoordinated(
|
|
655
|
+
input: IntegrationWriteInput,
|
|
656
|
+
options?: CoordinatedIntegrationOptions,
|
|
657
|
+
): Promise<WriteOutcome> {
|
|
658
|
+
return coordinatedWrite(input, refreshIntegration, options);
|
|
659
|
+
}
|
|
660
|
+
|
|
630
661
|
export function disableIntegrationCoordinated(
|
|
631
662
|
input: IntegrationWriteInput,
|
|
632
663
|
options?: CoordinatedIntegrationOptions,
|
|
@@ -52,6 +52,22 @@ function publisherForPrivateKey(privateKeyPem: string): PublicPublisherV1 {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The bounded errno-shaped code from a failed ACL harden, or null when the cause
|
|
57
|
+
* carries none.
|
|
58
|
+
*
|
|
59
|
+
* Only the code is allowed into the message. `hardenSecretPath` already sanitizes
|
|
60
|
+
* its own diagnostic prose, but this error is what reaches a CI log, so what
|
|
61
|
+
* crosses that boundary is re-checked here rather than trusted: an errno code has
|
|
62
|
+
* no separator, no lowercase and a bounded length, and therefore cannot carry the
|
|
63
|
+
* key pathname or the username component inside it.
|
|
64
|
+
*/
|
|
65
|
+
function aclFailureCode(error: unknown): string | null {
|
|
66
|
+
if (!(error instanceof Error) || !("code" in error)) return null;
|
|
67
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
68
|
+
return typeof code === "string" && /^[A-Z][A-Z0-9_]{1,30}$/.test(code) ? code : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
function requirePublisherKeyAcl(path: string, timeoutMemoKey = path): void {
|
|
56
72
|
privateRegularFileSize(path, PRIVATE_KEY_FILE_OPTIONS);
|
|
57
73
|
let hardened: { ok: boolean };
|
|
@@ -63,9 +79,17 @@ function requirePublisherKeyAcl(path: string, timeoutMemoKey = path): void {
|
|
|
63
79
|
hardened = { ok: false };
|
|
64
80
|
}
|
|
65
81
|
if (!hardened.ok) {
|
|
82
|
+
// Name the cause in the message, not only on `cause`. Every harden failure
|
|
83
|
+
// reaches a CI log as this one string, and the three that occur there need
|
|
84
|
+
// different fixes: ETIMEDOUT is the budget, EACLIDENTITY is the effective-SID
|
|
85
|
+
// lookup, EICACLS is icacls refusing the path. A message identical across all
|
|
86
|
+
// three cannot be acted on without a Windows box to re-run it under (#2152).
|
|
87
|
+
const code = aclFailureCode(hardeningError);
|
|
66
88
|
const failure = new PublicEvidenceValidationError(
|
|
67
89
|
"public_publisher_key_unsafe",
|
|
68
|
-
|
|
90
|
+
code
|
|
91
|
+
? `public publisher key ACL hardening did not complete (${code})`
|
|
92
|
+
: "public publisher key ACL hardening did not complete",
|
|
69
93
|
);
|
|
70
94
|
if (hardeningError !== undefined) {
|
|
71
95
|
(failure as Error & { cause?: unknown }).cause = hardeningError;
|
|
@@ -12,7 +12,7 @@ const CLOSED_KEYS = new Set([
|
|
|
12
12
|
"reasoning.supported", "reasoning.efforts", "reasoning.defaultEffort", "reasoning.effortMap", "reasoning.wireFormat",
|
|
13
13
|
"reasoning.summaryMode", "reasoning.replayMode", "reasoning.splitMode", "reasoning.toggleMode", "reasoning.budgetMode",
|
|
14
14
|
"tools.choiceRestrictions", "tools.parallel", "tools.hostedPreference", "tools.customFreeform", "tools.builtinNameEscaping",
|
|
15
|
-
"cache.forwarding", "cache.retention", "anthropic.eofPolicy",
|
|
15
|
+
"cache.forwarding", "cache.retention", "anthropic.eofPolicy", "openai-chat.eofPolicy",
|
|
16
16
|
"google.mode", "google.projectFingerprint", "google.locationFingerprint",
|
|
17
17
|
"openrouter.order", "openrouter.only", "openrouter.allowFallbacks",
|
|
18
18
|
"sidecars.vision", "sidecars.webSearch",
|
package/src/lib/redact.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const REDACTED_SECRET = "[REDACTED]";
|
|
2
2
|
|
|
3
|
-
const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn)$/i;
|
|
3
|
+
const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Colon-labelled credential headers echoed back inside an error body
|
|
@@ -33,7 +33,7 @@ const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-
|
|
|
33
33
|
// Every letter position also accepts \u0001, the placeholder the fold emits for
|
|
34
34
|
// an unresolved HTML named reference: `authorⅈzation` is the label with one
|
|
35
35
|
// character we cannot name, and that is still the label.
|
|
36
|
-
const CREDENTIAL_HEADER_LABEL_RAW = "x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
|
|
36
|
+
const CREDENTIAL_HEADER_LABEL_RAW = "x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
|
|
37
37
|
|
|
38
38
|
const CREDENTIAL_HEADER_LABEL = CREDENTIAL_HEADER_LABEL_RAW
|
|
39
39
|
.replace(/(?<![\[\\])([A-Za-z])(?![\]\-])/g, "[$1\u0001]");
|
package/src/oauth/log.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/oauth/log.ts
|
|
2
2
|
import { maskAccountId } from "../lib/privacy";
|
|
3
|
+
import { redactSecretString } from "../lib/redact";
|
|
3
4
|
|
|
4
5
|
/** Normalize camelCase / snake_case / kebab-case field names before secret checks. */
|
|
5
6
|
function normalizeFieldKey(key: string): string {
|
|
@@ -20,6 +21,7 @@ const FORBIDDEN_NORMALIZED = new Set([
|
|
|
20
21
|
"id_token",
|
|
21
22
|
"client_secret",
|
|
22
23
|
"oauth_code",
|
|
24
|
+
"code_verifier",
|
|
23
25
|
"clientsecret",
|
|
24
26
|
]);
|
|
25
27
|
|
|
@@ -44,5 +46,5 @@ export function logOAuthEvent(
|
|
|
44
46
|
if (value === undefined) continue;
|
|
45
47
|
parts.push(`${key}=${String(value)}`);
|
|
46
48
|
}
|
|
47
|
-
console.info(parts.join(" "));
|
|
49
|
+
console.info(redactSecretString(parts.join(" ")));
|
|
48
50
|
}
|
package/src/providers/derive.ts
CHANGED
|
@@ -47,6 +47,7 @@ export interface DerivedKeyLoginProvider {
|
|
|
47
47
|
thinkingToggleModels?: string[];
|
|
48
48
|
thinkingBudgetModels?: string[];
|
|
49
49
|
escapeBuiltinToolNames?: boolean;
|
|
50
|
+
openaiChatEofTolerance?: boolean;
|
|
50
51
|
googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
|
|
51
52
|
project?: string;
|
|
52
53
|
location?: string;
|
|
@@ -247,6 +248,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
247
248
|
...(entry.parallelToolCalls !== undefined ? { parallelToolCalls: entry.parallelToolCalls } : {}),
|
|
248
249
|
...(entry.promptCacheKey !== undefined ? { promptCacheKey: entry.promptCacheKey } : {}),
|
|
249
250
|
...(entry.chatServiceTier !== undefined ? { chatServiceTier: entry.chatServiceTier } : {}),
|
|
251
|
+
...(entry.openaiChatEofTolerance !== undefined ? { openaiChatEofTolerance: entry.openaiChatEofTolerance } : {}),
|
|
250
252
|
...(entry.responsesPath !== undefined ? { responsesPath: entry.responsesPath } : {}),
|
|
251
253
|
...(entry.statelessResponses !== undefined ? { statelessResponses: entry.statelessResponses } : {}),
|
|
252
254
|
...(entry.requiresAdjacentResponsesToolResults !== undefined
|
|
@@ -306,6 +308,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
|
|
|
306
308
|
...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
|
|
307
309
|
...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}),
|
|
308
310
|
...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}),
|
|
311
|
+
...(entry.openaiChatEofTolerance !== undefined ? { openaiChatEofTolerance: entry.openaiChatEofTolerance } : {}),
|
|
309
312
|
...(entry.googleMode ? { googleMode: entry.googleMode } : {}),
|
|
310
313
|
...(entry.project ? { project: entry.project } : {}),
|
|
311
314
|
...(entry.location ? { location: entry.location } : {}),
|
|
@@ -461,6 +464,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
461
464
|
if (prov.parallelToolCalls === undefined && seed.parallelToolCalls !== undefined) prov.parallelToolCalls = seed.parallelToolCalls;
|
|
462
465
|
if (prov.promptCacheKey === undefined && seed.promptCacheKey !== undefined) prov.promptCacheKey = seed.promptCacheKey;
|
|
463
466
|
if (prov.chatServiceTier === undefined && seed.chatServiceTier !== undefined) prov.chatServiceTier = seed.chatServiceTier;
|
|
467
|
+
if (prov.openaiChatEofTolerance === undefined && seed.openaiChatEofTolerance !== undefined) {
|
|
468
|
+
prov.openaiChatEofTolerance = seed.openaiChatEofTolerance;
|
|
469
|
+
}
|
|
464
470
|
// Fill-only: a hand-edited path must survive, and a config saved before the registry
|
|
465
471
|
// learned this route still gets backfilled.
|
|
466
472
|
if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
|
|
@@ -474,6 +480,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
474
480
|
prov.fastWire = cloneFastWire(entry.fastWire);
|
|
475
481
|
}
|
|
476
482
|
if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
|
|
483
|
+
if (prov.supportsOpenAiWebSearchToolFields === undefined && entry.supportsOpenAiWebSearchToolFields !== undefined) {
|
|
484
|
+
prov.supportsOpenAiWebSearchToolFields = entry.supportsOpenAiWebSearchToolFields;
|
|
485
|
+
}
|
|
477
486
|
if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
|
|
478
487
|
applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
|
|
479
488
|
applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(entry, prov));
|
|
@@ -143,34 +143,40 @@ function resolvePolicyAdapter(
|
|
|
143
143
|
inbound: InboundWire,
|
|
144
144
|
): { adapter: string; hardPinned: boolean; forwardCallerServiceTier?: boolean } {
|
|
145
145
|
// Hard pins and configured overrides deliberately use the same exact-key semantics as
|
|
146
|
-
// resolveWireProtocolOverride(). Registry
|
|
146
|
+
// resolveWireProtocolOverride(). Registry route policy normalizes ids at its boundary.
|
|
147
147
|
const hardPin = Object.hasOwn(authority.hardPins, modelId)
|
|
148
148
|
? authority.hardPins[modelId]
|
|
149
149
|
: undefined;
|
|
150
150
|
if (typeof hardPin === "string") return { adapter: hardPin, hardPinned: true };
|
|
151
151
|
if (authority.modelWireOverrideAllowed) {
|
|
152
|
+
const registryDefault = MODEL_ADAPTER_OVERRIDE_ALLOWED.has(authority.providerAdapter)
|
|
153
|
+
? registryDefaultForModel(
|
|
154
|
+
authority.registryWireDefaults,
|
|
155
|
+
modelId,
|
|
156
|
+
inbound,
|
|
157
|
+
authority.providerAuthMode,
|
|
158
|
+
)
|
|
159
|
+
: undefined;
|
|
152
160
|
const configured = Object.hasOwn(authority.modelAdapters, modelId)
|
|
153
161
|
? authority.modelAdapters[modelId]
|
|
154
162
|
: undefined;
|
|
155
163
|
if (typeof configured === "string" && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configured)) {
|
|
156
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
adapter: configured,
|
|
166
|
+
hardPinned: false,
|
|
167
|
+
...(registryDefault?.forwardCallerServiceTier === false
|
|
168
|
+
? { forwardCallerServiceTier: false }
|
|
169
|
+
: {}),
|
|
170
|
+
};
|
|
157
171
|
}
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
adapter: registryDefault.adapter,
|
|
168
|
-
hardPinned: false,
|
|
169
|
-
...(registryDefault.forwardCallerServiceTier !== undefined
|
|
170
|
-
? { forwardCallerServiceTier: registryDefault.forwardCallerServiceTier }
|
|
171
|
-
: {}),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
172
|
+
if (registryDefault !== undefined) {
|
|
173
|
+
return {
|
|
174
|
+
adapter: registryDefault.adapter,
|
|
175
|
+
hardPinned: false,
|
|
176
|
+
...(registryDefault.forwardCallerServiceTier !== undefined
|
|
177
|
+
? { forwardCallerServiceTier: registryDefault.forwardCallerServiceTier }
|
|
178
|
+
: {}),
|
|
179
|
+
};
|
|
174
180
|
}
|
|
175
181
|
}
|
|
176
182
|
return { adapter: authority.providerAdapter, hardPinned: false };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig, ProviderCostOverlay } from "../types";
|
|
2
2
|
import { OPENAI_PROVIDER_TIER_VERSION } from "../types";
|
|
3
|
+
import { openaiResponsesUrl } from "../adapters/openai-responses-url";
|
|
3
4
|
import { MAX_COST4_RATE } from "../usage/expected-prices";
|
|
4
5
|
|
|
5
6
|
export const OPENAI_CODEX_PROVIDER_ID = "openai";
|
|
@@ -36,7 +37,35 @@ export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): b
|
|
|
36
37
|
&& normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
const
|
|
40
|
+
const OPENAI_API_ORIGIN = "https://api.openai.com";
|
|
41
|
+
const OPENAI_API_BASE_URL = `${OPENAI_API_ORIGIN}/v1`;
|
|
42
|
+
const OPENAI_API_RESPONSES_URL = `${OPENAI_API_BASE_URL}/responses`;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The Responses endpoint the adapter would actually POST key-auth traffic to, normalized.
|
|
46
|
+
*
|
|
47
|
+
* Mirrors the adapter's own construction (`src/adapters/openai-responses.ts`): a configured
|
|
48
|
+
* `responsesPath` is appended to the base verbatim, and only the default branch runs the
|
|
49
|
+
* `/v1/responses` suffix normalization. Classifying on the base URL alone would call
|
|
50
|
+
* `baseUrl: "https://api.openai.com"` with `responsesPath: "/other"` official even though that
|
|
51
|
+
* request never reaches the official Responses endpoint.
|
|
52
|
+
*/
|
|
53
|
+
function resolvedResponsesEndpoint(provider: OcxProviderConfig): string | undefined {
|
|
54
|
+
try {
|
|
55
|
+
const raw = provider.responsesPath === undefined
|
|
56
|
+
? openaiResponsesUrl(provider.baseUrl)
|
|
57
|
+
: `${provider.baseUrl.replace(/\/$/, "")}${provider.responsesPath}`;
|
|
58
|
+
return normalizedBaseUrl(raw);
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isOfficialOpenAiResponsesDestination(provider: OcxProviderConfig): boolean {
|
|
65
|
+
// Exact normalized URL keeps lookalike/suffix hosts out of this set: `api.openai.com.evil.test`
|
|
66
|
+
// resolves to its own origin, never to the official one.
|
|
67
|
+
return resolvedResponsesEndpoint(provider) === OPENAI_API_RESPONSES_URL;
|
|
68
|
+
}
|
|
40
69
|
|
|
41
70
|
/**
|
|
42
71
|
* Whether this provider can serve `POST /responses/compact`. The canonical ChatGPT
|
|
@@ -54,6 +83,36 @@ export function supportsNativeResponsesCompactEndpoint(
|
|
|
54
83
|
&& normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL;
|
|
55
84
|
}
|
|
56
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Whether this destination is an OpenAI-operated Responses backend — the canonical ChatGPT Codex
|
|
88
|
+
* surface or the official OpenAI API.
|
|
89
|
+
*
|
|
90
|
+
* Deliberately not keyed on `authMode === "forward"`: a noncanonical forward provider does not
|
|
91
|
+
* receive the caller's credentials (see the forward-header gate in the Responses adapter), so
|
|
92
|
+
* forward auth says nothing about which backend is on the other end.
|
|
93
|
+
*/
|
|
94
|
+
export function isOpenAiOperatedResponsesDestination(provider: OcxProviderConfig): boolean {
|
|
95
|
+
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
96
|
+
return provider.adapter === "openai-responses"
|
|
97
|
+
&& isOfficialOpenAiResponsesDestination(provider);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether this destination can decode a native (non-`ocx1:`) compaction blob.
|
|
102
|
+
*
|
|
103
|
+
* Only the backend that minted a blob can decode it. `authMode: "forward"` alone is not a signal:
|
|
104
|
+
* the adapter forwards caller credentials only to the canonical ChatGPT Codex surface, while a
|
|
105
|
+
* noncanonical forward provider receives no caller credentials and may point at any backend.
|
|
106
|
+
*
|
|
107
|
+
* Relay only to an OpenAI-operated destination or a destination whose operator explicitly opts in.
|
|
108
|
+
* Keyed by destination rather than provider id: a blob's issuer is the URL that produced it, not the
|
|
109
|
+
* local config key a replay travels under.
|
|
110
|
+
*/
|
|
111
|
+
export function destinationDecodesNativeCompactionBlob(provider: OcxProviderConfig): boolean {
|
|
112
|
+
return isOpenAiOperatedResponsesDestination(provider)
|
|
113
|
+
|| provider.decodesNativeCompactionBlobs === true;
|
|
114
|
+
}
|
|
115
|
+
|
|
57
116
|
export interface OpenAiTierMigrationProjection {
|
|
58
117
|
config: OcxConfig;
|
|
59
118
|
changed: boolean;
|
|
@@ -222,6 +222,8 @@ export interface ProviderRegistryEntry {
|
|
|
222
222
|
* (and the canonical openai seed comparison keeps its exact key set).
|
|
223
223
|
*/
|
|
224
224
|
supportsServiceTier?: boolean;
|
|
225
|
+
/** Registry default for OpenAI extended hosted web_search field support. */
|
|
226
|
+
supportsOpenAiWebSearchToolFields?: boolean;
|
|
225
227
|
/** Registry default for exact model service-tier capability; explicit config keys win. */
|
|
226
228
|
modelSupportsServiceTier?: Record<string, boolean>;
|
|
227
229
|
/**
|
|
@@ -266,6 +268,8 @@ export interface ProviderRegistryEntry {
|
|
|
266
268
|
* `supportsServiceTier`, which governs the Responses wire.
|
|
267
269
|
*/
|
|
268
270
|
chatServiceTier?: boolean;
|
|
271
|
+
/** OpenAI Chat EOF policy for gateways that omit terminal frames after complete tool calls. */
|
|
272
|
+
openaiChatEofTolerance?: boolean;
|
|
269
273
|
autoToolChoiceOnlyModels?: string[];
|
|
270
274
|
preserveReasoningContentModels?: string[];
|
|
271
275
|
requiresReasoningPlaceholderModels?: string[];
|
|
@@ -291,7 +295,7 @@ export type ProviderConfigSeed = Pick<
|
|
|
291
295
|
| "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens"
|
|
292
296
|
| "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat"
|
|
293
297
|
| "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
|
|
294
|
-
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
|
|
298
|
+
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance"
|
|
295
299
|
| "googleMode" | "project" | "location" | "headers"
|
|
296
300
|
>;
|
|
297
301
|
|
|
@@ -1006,6 +1010,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1006
1010
|
featured: true,
|
|
1007
1011
|
oauthId: "xai",
|
|
1008
1012
|
jawcodeBundle: "xai",
|
|
1013
|
+
supportsOpenAiWebSearchToolFields: false,
|
|
1009
1014
|
note: "Log in with your Grok account",
|
|
1010
1015
|
// Parallel tool calls: officially supported and default-on per docs.x.ai function-calling
|
|
1011
1016
|
// (verified 260709, devlog/_plan/260709_parallel_tool_calls). Streamed calls arrive whole
|
|
@@ -1023,19 +1028,18 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1023
1028
|
// grok-4.5; the reasoning ladder does not — 4.6 adds the documented xhigh rung.
|
|
1024
1029
|
models: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
|
|
1025
1030
|
defaultModel: "grok-4.5",
|
|
1026
|
-
//
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1029
|
-
// Chat Completions compatibility stream to flush at the end of a reasoning turn.
|
|
1031
|
+
// Keep Codex Responses callers on the compatibility Chat wire until xAI can replay
|
|
1032
|
+
// opaque reasoning continuation and compaction state across later turns. The scoped
|
|
1033
|
+
// declaration also keeps caller-owned service tiers off the OAuth subscription route.
|
|
1030
1034
|
modelWireDefaults: {
|
|
1031
1035
|
"grok-4.6": {
|
|
1032
|
-
wire: "openai-
|
|
1036
|
+
wire: "openai-chat",
|
|
1033
1037
|
inbound: ["responses"],
|
|
1034
1038
|
authModes: ["oauth"],
|
|
1035
1039
|
forwardCallerServiceTier: false,
|
|
1036
1040
|
},
|
|
1037
1041
|
"grok-4.5": {
|
|
1038
|
-
wire: "openai-
|
|
1042
|
+
wire: "openai-chat",
|
|
1039
1043
|
inbound: ["responses"],
|
|
1040
1044
|
authModes: ["oauth"],
|
|
1041
1045
|
forwardCallerServiceTier: false,
|
|
@@ -1276,6 +1280,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1276
1280
|
id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1",
|
|
1277
1281
|
authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
|
|
1278
1282
|
jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…",
|
|
1283
|
+
// Zen Go can close a Chat stream after a fully assembled function call without sending
|
|
1284
|
+
// finish_reason or [DONE] (#2260). The adapter still rejects incomplete argument JSON.
|
|
1285
|
+
openaiChatEofTolerance: true,
|
|
1279
1286
|
/* [Decision Log]
|
|
1280
1287
|
- 목적과 의도: Route GPT 5.6 Luna to the Responses endpoint that OpenCode Go documents for that exact model.
|
|
1281
1288
|
- 기존 구현 및 제약 조건: The provider is mixed-wire but its provider-wide `openai-chat` adapter sent Luna to `/chat/completions`; explicit user `modelAdapters` entries must remain authoritative.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export const XAI_RESPONSES_OPT_IN_MODELS = ["grok-4.6", "grok-4.5"] as const;
|
|
4
|
+
|
|
5
|
+
export type XaiResponsesOptInState = boolean | "mixed";
|
|
6
|
+
|
|
7
|
+
/** Derived dashboard/API state for the two modelAdapters entries owned by the xAI opt-in. */
|
|
8
|
+
export function xaiResponsesOptInState(provider: OcxProviderConfig): XaiResponsesOptInState {
|
|
9
|
+
const enabled = XAI_RESPONSES_OPT_IN_MODELS.map(
|
|
10
|
+
model => provider.modelAdapters?.[model] === "openai-responses",
|
|
11
|
+
);
|
|
12
|
+
if (enabled.every(Boolean)) return true;
|
|
13
|
+
if (enabled.some(Boolean)) return "mixed";
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
@@ -33,6 +33,24 @@ export const SUMMARY_PREFIX = "Another language model started to solve this prob
|
|
|
33
33
|
|
|
34
34
|
export const OPAQUE_COMPACTION_NOTE = "[earlier conversation was compacted; the summary is stored in a format this model cannot read]";
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Item types in the compact wire family. Each carries an `encrypted_content` blob the client
|
|
38
|
+
* replays verbatim on every later turn, and the minting backend verifies it is unmodified.
|
|
39
|
+
*
|
|
40
|
+
* Keep this the only enumeration: a copy that listed just `compaction` let the response-side
|
|
41
|
+
* field backfill synthesize ids into the other two, which the client then replayed as "modified
|
|
42
|
+
* from the compact response".
|
|
43
|
+
*/
|
|
44
|
+
const COMPACTION_ITEM_TYPES: ReadonlySet<string> = new Set([
|
|
45
|
+
"compaction",
|
|
46
|
+
"compaction_summary",
|
|
47
|
+
"context_compaction",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export function isCompactionItemType(type: unknown): boolean {
|
|
51
|
+
return typeof type === "string" && COMPACTION_ITEM_TYPES.has(type);
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
export function encodeCompactionSummary(summary: string): string {
|
|
37
55
|
return OCX_COMPACTION_PREFIX + Buffer.from(summary, "utf-8").toString("base64");
|
|
38
56
|
}
|