@bitkyc08/opencodex 2.24.2 → 2.25.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/gui/dist/assets/{index-DW-DYWmz.js → index-DxJ7kXj9.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +42 -0
- package/src/adapters/client-fingerprint.ts +9 -5
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +17 -0
- package/src/adapters/cursor/cursor-errors.ts +49 -0
- package/src/adapters/cursor/live-models.ts +36 -2
- package/src/adapters/cursor/live-transport.ts +55 -4
- package/src/adapters/cursor/native-exec.ts +9 -0
- package/src/adapters/cursor/protobuf-request.ts +160 -9
- package/src/adapters/cursor/request-builder.ts +9 -1
- package/src/adapters/cursor/tool-definitions.ts +7 -2
- package/src/adapters/google-antigravity-wire.ts +1 -1
- package/src/adapters/google.ts +30 -12
- package/src/adapters/openai-responses-url.ts +5 -3
- package/src/adapters/registry.ts +3 -1
- package/src/adapters/tool-catalog-nudge.ts +76 -9
- package/src/bridge.ts +53 -9
- package/src/codex/app-server-processes.ts +69 -35
- package/src/codex/catalog/provider-fetch.ts +5 -1
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +40 -32
- package/src/lib/windows-elevation.ts +8 -2
- package/src/oauth/google-antigravity.ts +7 -2
- package/src/providers/antigravity-models.ts +126 -17
- package/src/providers/derive.ts +11 -1
- package/src/responses/parser.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +16 -1
- package/src/responses/thought-signature-replay.ts +17 -1
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +2 -10
- package/src/server/management/provider-routes.ts +22 -0
- package/src/server/request-log.ts +11 -3
- package/src/server/responses/core.ts +2 -0
- package/src/types.ts +13 -1
package/src/adapters/google.ts
CHANGED
|
@@ -48,19 +48,17 @@ const GOOGLE_BREVITY_INSTRUCTION = [
|
|
|
48
48
|
].join("\n");
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Google
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* id must be resolved here before it reaches the URL. The user-facing id is deliberately
|
|
55
|
-
* left alone: the picker, the catalog, the usage log and the price overlays all stay
|
|
56
|
-
* keyed on the base id, and only the wire path learns the new spelling.
|
|
51
|
+
* Some Google direct deployments expose current Gemini Flash generations with a `-tiered`
|
|
52
|
+
* wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id
|
|
53
|
+
* stable and make the mapping configurable for deployments that still serve the bare id.
|
|
57
54
|
*/
|
|
58
55
|
const GEMINI_DIRECT_WIRE_RENAMES: Record<string, string> = {
|
|
59
56
|
"gemini-3.7-flash": "gemini-3.7-flash-tiered",
|
|
60
57
|
"gemini-3.6-flash": "gemini-3.6-flash-tiered",
|
|
61
58
|
};
|
|
62
59
|
|
|
63
|
-
function resolveDirectGeminiWireModelId(modelId: string): string {
|
|
60
|
+
function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean): string {
|
|
61
|
+
if (!applyRenames) return modelId;
|
|
64
62
|
return Object.hasOwn(GEMINI_DIRECT_WIRE_RENAMES, modelId)
|
|
65
63
|
? GEMINI_DIRECT_WIRE_RENAMES[modelId]!
|
|
66
64
|
: modelId;
|
|
@@ -147,7 +145,7 @@ function geminiToolResultText(content: string | OcxContentPart[]): string {
|
|
|
147
145
|
|
|
148
146
|
function messagesToGeminiFormat(
|
|
149
147
|
parsed: OcxParsedRequest,
|
|
150
|
-
|
|
148
|
+
identityModelId: string,
|
|
151
149
|
): { systemInstruction?: unknown; contents: unknown[] } {
|
|
152
150
|
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
153
151
|
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
@@ -156,7 +154,7 @@ function messagesToGeminiFormat(
|
|
|
156
154
|
...(parsed.context.systemPrompt ?? []),
|
|
157
155
|
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
158
156
|
GOOGLE_BREVITY_INSTRUCTION,
|
|
159
|
-
].join("\n\n"),
|
|
157
|
+
].join("\n\n"), identityModelId);
|
|
160
158
|
const systemInstruction = { parts: [{ text: systemText }] };
|
|
161
159
|
|
|
162
160
|
const contents: unknown[] = [];
|
|
@@ -395,9 +393,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
395
393
|
? resolveAntigravityEffortWireModel(
|
|
396
394
|
parsed.modelId,
|
|
397
395
|
mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
|
|
396
|
+
provider.baseUrl,
|
|
398
397
|
).wireModelId
|
|
399
|
-
:
|
|
400
|
-
|
|
398
|
+
: provider.googleMode === "vertex"
|
|
399
|
+
? parsed.modelId
|
|
400
|
+
: resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
|
|
401
|
+
// AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
|
|
402
|
+
const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
|
|
403
|
+
const { systemInstruction, contents } = messagesToGeminiFormat(parsed, identityModelId);
|
|
401
404
|
const tools = toolsToGeminiFormat(parsed);
|
|
402
405
|
|
|
403
406
|
const body: Record<string, unknown> = { contents };
|
|
@@ -450,7 +453,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
450
453
|
if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
|
|
451
454
|
const sessionId = antigravitySessionId(parsed);
|
|
452
455
|
const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
453
|
-
const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(
|
|
456
|
+
const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(
|
|
457
|
+
parsed.modelId,
|
|
458
|
+
mappedEffort,
|
|
459
|
+
provider.baseUrl,
|
|
460
|
+
);
|
|
454
461
|
antigravityModel = wireModelId;
|
|
455
462
|
antigravitySession = sessionId;
|
|
456
463
|
// Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
|
|
@@ -939,9 +946,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
939
946
|
}
|
|
940
947
|
|
|
941
948
|
const usage = json.usageMetadata as Record<string, number> | undefined;
|
|
949
|
+
// Mirror the streaming path: a buffered turn cut off by the token limit or a content filter
|
|
950
|
+
// must carry its stop reason, or the bridge sees a clean `done` and reports the truncated
|
|
951
|
+
// turn as completed — and, on a compaction turn, installs the half-written summary as
|
|
952
|
+
// replacement history (#422).
|
|
953
|
+
const finishReason = candidates?.[0]?.finishReason as string | undefined;
|
|
954
|
+
const stopReason = finishReason === "MAX_TOKENS"
|
|
955
|
+
? "max_tokens"
|
|
956
|
+
: ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(finishReason ?? "")
|
|
957
|
+
? "content_filter"
|
|
958
|
+
: undefined;
|
|
942
959
|
events.push({
|
|
943
960
|
type: "done",
|
|
944
961
|
usage: usageFromGemini(usage),
|
|
962
|
+
...(stopReason ? { stopReason } : {}),
|
|
945
963
|
});
|
|
946
964
|
return finish(events);
|
|
947
965
|
},
|
|
@@ -7,8 +7,10 @@ const TRAILING_V1 = /\/v1\/?$/;
|
|
|
7
7
|
* Custom `responsesPath` stays on the adapter; this helper is only the legacy /v1/responses branch.
|
|
8
8
|
*/
|
|
9
9
|
export function openaiResponsesUrl(baseUrl: string): string {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const url = new URL(baseUrl.trim());
|
|
11
|
+
const trimmedPath = url.pathname.replace(TRAILING_SLASHES, "");
|
|
12
|
+
const withoutEndpoint = trimmedPath.replace(TRAILING_RESPONSES, "");
|
|
12
13
|
const withoutV1 = withoutEndpoint.replace(TRAILING_V1, "");
|
|
13
|
-
|
|
14
|
+
url.pathname = `${withoutV1}/v1/responses`;
|
|
15
|
+
return url.toString();
|
|
14
16
|
}
|
package/src/adapters/registry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAnthropicAdapter } from "./anthropic";
|
|
2
2
|
import { createAzureAdapter } from "./azure";
|
|
3
3
|
import type { ProviderAdapter } from "./base";
|
|
4
|
+
import { withClinePassDeepSeekV4ToolReplayCompatibility } from "./cline-pass-deepseek-v4-tool-replay";
|
|
4
5
|
import { createCommandCodeAdapter } from "./command-code";
|
|
5
6
|
import { createCursorAdapter } from "./cursor";
|
|
6
7
|
import { createGoogleAdapter } from "./google";
|
|
@@ -57,7 +58,8 @@ export const ADAPTER_REGISTRY = {
|
|
|
57
58
|
"openai-chat": {
|
|
58
59
|
wire: "openai-chat",
|
|
59
60
|
mutation: "codex-owned",
|
|
60
|
-
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) =>
|
|
61
|
+
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) =>
|
|
62
|
+
withClinePassDeepSeekV4ToolReplayCompatibility(createOpenAIChatAdapter(provider)),
|
|
61
63
|
},
|
|
62
64
|
anthropic: {
|
|
63
65
|
wire: "anthropic",
|
|
@@ -17,8 +17,40 @@ import {
|
|
|
17
17
|
// included it either.
|
|
18
18
|
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The two halves of the code-mode shape, kept provider-neutral here.
|
|
22
|
+
*
|
|
23
|
+
* `./cursor/tool-definitions.ts` owns the Cursor-scoped versions of these
|
|
24
|
+
* (`isCursorCodeModeExecTool` / `isBareCodexShellBridgeTool`), but those additionally require
|
|
25
|
+
* the Cursor Responses namespace. This nudge is shared by Anthropic, Google, Kiro,
|
|
26
|
+
* OpenAI-chat and command-code, so it needs the same semantics without that provider gate.
|
|
27
|
+
*/
|
|
28
|
+
const CODEX_UNIFIED_EXEC_TOOL_NAME = "exec";
|
|
29
|
+
const CODEX_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const;
|
|
30
|
+
|
|
31
|
+
function isCodexCodeModeExecTool(tool: Pick<OcxTool, "namespace" | "name" | "freeform">): boolean {
|
|
32
|
+
return !tool.namespace && tool.name === CODEX_UNIFIED_EXEC_TOOL_NAME && tool.freeform === true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* BARE means un-namespaced, and the word is load-bearing.
|
|
37
|
+
*
|
|
38
|
+
* An MCP server can advertise its own `exec_command` or `shell_command` — a docker, k8s or ssh
|
|
39
|
+
* server plausibly does — and those arrive namespaced (`mcp__docker__exec_command`). They are
|
|
40
|
+
* not Codex's shell bridge, so they must not cancel code mode: a genuine code-mode turn that
|
|
41
|
+
* merely happens to sit beside an MCP shell tool would lose its guidance and fall back to the
|
|
42
|
+
* generic sentence.
|
|
43
|
+
*
|
|
44
|
+
* The Cursor original this was ported from (`isBareCodexShellBridgeTool`) carries the same
|
|
45
|
+
* `!tool.namespace` requirement; dropping it here made the name assert a check the body did not
|
|
46
|
+
* perform.
|
|
47
|
+
*/
|
|
48
|
+
function isBareShellBridgeTool(tool: Pick<OcxTool, "namespace" | "name">): boolean {
|
|
49
|
+
return !tool.namespace && (CODEX_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(tool.name);
|
|
50
|
+
}
|
|
51
|
+
|
|
20
52
|
function quoteNames(names: readonly string[]): string {
|
|
21
|
-
return names.map(name =>
|
|
53
|
+
return names.map(name => "`" + name + "`").join(", ");
|
|
22
54
|
}
|
|
23
55
|
|
|
24
56
|
function uniqueNames(names: readonly string[]): string[] {
|
|
@@ -40,9 +72,33 @@ export function shouldInjectNonOpenAIToolCatalogNudge(provider: Pick<OcxProvider
|
|
|
40
72
|
}
|
|
41
73
|
}
|
|
42
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Codex code mode is a SEMANTIC property, not a name.
|
|
77
|
+
*
|
|
78
|
+
* The tool that carries it is a `freeform` `exec` whose body is JavaScript evaluated in a V8
|
|
79
|
+
* isolate, advertised alongside no bare shell bridge. A provider is free to advertise an
|
|
80
|
+
* ordinary structured tool called `exec` that runs a shell string — and a catalog can list
|
|
81
|
+
* `exec` next to `exec_command`/`shell_command`, which is the flat-bridge shape, not code mode.
|
|
82
|
+
*
|
|
83
|
+
* Classifying on the name alone would tell those turns that `exec` takes JavaScript and that
|
|
84
|
+
* shell is only reachable as a nested `tools.*` helper. Both are false there, and a model that
|
|
85
|
+
* believes them sends the wrong arguments or avoids a legitimate execution tool entirely.
|
|
86
|
+
*
|
|
87
|
+
* So callers that HAVE the tool objects decide with the semantic predicate and pass the verified
|
|
88
|
+
* wire name in; the name-only entry point cannot decide it and does not try.
|
|
89
|
+
*/
|
|
90
|
+
function codeModeExecWireName(
|
|
91
|
+
advertised: ReadonlySet<string>,
|
|
92
|
+
verifiedName: string | undefined,
|
|
93
|
+
): string | undefined {
|
|
94
|
+
if (!verifiedName) return undefined;
|
|
95
|
+
return advertised.has(verifiedName) ? verifiedName : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
43
98
|
export function buildNonOpenAIToolCatalogNudgeFromNames(
|
|
44
99
|
wireNames: readonly string[] | undefined,
|
|
45
100
|
toWireName: (name: string) => string = name => name,
|
|
101
|
+
codeModeExecName?: string,
|
|
46
102
|
): string | undefined {
|
|
47
103
|
const names = uniqueNames(wireNames ?? []);
|
|
48
104
|
if (names.length === 0) return undefined;
|
|
@@ -50,21 +106,24 @@ export function buildNonOpenAIToolCatalogNudgeFromNames(
|
|
|
50
106
|
const advertised = new Set(names);
|
|
51
107
|
// Compare in the catalog's own coordinate system. `advertised` holds WIRE names, so a
|
|
52
108
|
// provider that rewrites them (Claude OAuth `custom_`, Anthropic compat `cx_`) would never
|
|
53
|
-
// match a bare neighbor name and would forbid tools the turn actually advertises
|
|
109
|
+
// match a bare neighbor name and would forbid tools the turn actually advertises -- the
|
|
54
110
|
// catalog would list `custom_apply_patch` while the same sentence banned `apply_patch`.
|
|
55
111
|
const unavailableNeighborNames = NEIGHBOR_AGENT_TOOL_NAMES.filter(
|
|
56
112
|
name => !advertised.has(name) && !advertised.has(toWireName(name)),
|
|
57
113
|
);
|
|
114
|
+
const verifiedCodeModeExecName = codeModeExecWireName(advertised, codeModeExecName);
|
|
58
115
|
|
|
59
116
|
return [
|
|
60
117
|
"Tool contract: use the current tool catalog as ground truth.",
|
|
61
|
-
|
|
118
|
+
"Valid tool names for this turn are exactly " + quoteNames(names) + ".",
|
|
62
119
|
"These listed names are the complete top-level tool-call surface for this turn.",
|
|
63
120
|
"Call only listed names with their listed argument keys; do not invent, translate, or rename tools.",
|
|
64
121
|
"Names mentioned only in instructions, tool descriptions, argument descriptions, or nested helper APIs are not additional top-level tools.",
|
|
65
|
-
|
|
122
|
+
verifiedCodeModeExecName
|
|
123
|
+
? "`" + verifiedCodeModeExecName + "` is Codex code mode: its body is JavaScript evaluated in a V8 isolate. Nested helpers are called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})` or `await tools.codex_app__list_threads({})`. Absence from the top-level catalog or from `" + verifiedCodeModeExecName + "`'s description is not absence: deferred helpers stay callable on `tools.<name>`. Discover them from the isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`. Do not skip an available nested helper because it is omitted from the listed top-level names."
|
|
124
|
+
: "If a listed tool exposes nested helpers such as a tools.* API, call the listed parent tool and use those helpers only inside that tool's input.",
|
|
66
125
|
unavailableNeighborNames.length > 0
|
|
67
|
-
?
|
|
126
|
+
? "Do not use neighboring-agent tool names " + quoteNames(unavailableNeighborNames) + " unless this turn's catalog lists those exact names."
|
|
68
127
|
: undefined,
|
|
69
128
|
"If you need shell, file search, file read, edit, or discovery behavior, choose the listed tool that provides that capability.",
|
|
70
129
|
"Count a tool call only after its tool result returns; batch independent read-only calls when the runtime supports it.",
|
|
@@ -72,16 +131,24 @@ export function buildNonOpenAIToolCatalogNudgeFromNames(
|
|
|
72
131
|
}
|
|
73
132
|
|
|
74
133
|
export function buildNonOpenAIToolCatalogNudgeForTools(
|
|
75
|
-
tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
|
|
134
|
+
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
|
|
76
135
|
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
77
136
|
toWireName: (tool: Pick<OcxTool, "namespace" | "name">) => string = tool => namespacedToolName(tool.namespace, tool.name),
|
|
78
137
|
): string | undefined {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
138
|
+
const visible = tools?.filter(toolChoiceToolPredicate(toolChoice));
|
|
139
|
+
const visibleNames = visible?.map(toWireName);
|
|
140
|
+
// Decide code mode from the tool OBJECTS, while the `freeform` flag still exists — reducing
|
|
141
|
+
// to wire names first throws away the only thing that distinguishes Codex's JavaScript
|
|
142
|
+
// `exec` from an ordinary structured tool that happens to share the name.
|
|
143
|
+
const codeModeExecTool = visible?.find(isCodexCodeModeExecTool);
|
|
144
|
+
const codeModeExecName = codeModeExecTool
|
|
145
|
+
&& !visible?.some(isBareShellBridgeTool)
|
|
146
|
+
? toWireName(codeModeExecTool)
|
|
147
|
+
: undefined;
|
|
82
148
|
// Neighbor names are bare and un-namespaced, so probe the same transform with a bare tool.
|
|
83
149
|
return buildNonOpenAIToolCatalogNudgeFromNames(
|
|
84
150
|
visibleNames,
|
|
85
151
|
name => toWireName({ name }),
|
|
152
|
+
codeModeExecName,
|
|
86
153
|
);
|
|
87
154
|
}
|
package/src/bridge.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import { coerceIntegerToolArguments } from "./lib/tool-argument-integers";
|
|
10
10
|
import { adapterFailureFromMessage, classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, type OcxErrorPayload } from "./lib/errors";
|
|
11
11
|
import { encodeCompactionSummary } from "./responses/compaction";
|
|
12
|
+
import { isTruncatedStopReason, truncationReasonFor } from "./responses/truncated-stop-reason";
|
|
12
13
|
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
13
14
|
import { rememberReasoningForCall } from "./responses/reasoning-replay-cache";
|
|
14
15
|
import {
|
|
@@ -1154,7 +1155,11 @@ export function bridgeToResponsesSSE(
|
|
|
1154
1155
|
// After every close above, so the blob lands AFTER the assistant message it belongs
|
|
1155
1156
|
// to and the parser's backwards pairing finds it.
|
|
1156
1157
|
flushKiroRedactedReasoning();
|
|
1157
|
-
|
|
1158
|
+
// Truncated turns must never install replacement history (#422). The buffered path
|
|
1159
|
+
// has always checked this; streaming emitted the item BEFORE reading stopReason, so
|
|
1160
|
+
// a max_tokens/content_filter turn shipped a half-written summary and then declared
|
|
1161
|
+
// itself incomplete — the same hazard, one branch over.
|
|
1162
|
+
if (options?.compaction && !isTruncatedStopReason(event.stopReason)) {
|
|
1158
1163
|
// Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
|
|
1159
1164
|
const item = {
|
|
1160
1165
|
type: "compaction", id: `cmp_${uuid()}`,
|
|
@@ -1164,14 +1169,18 @@ export function bridgeToResponsesSSE(
|
|
|
1164
1169
|
retainFinishedItem(item as OutputItem, compactionTextBytes);
|
|
1165
1170
|
outputIndex++;
|
|
1166
1171
|
}
|
|
1167
|
-
|
|
1172
|
+
// Recognize every adapter's truncation vocabulary, not just the canonical pair.
|
|
1173
|
+
// Suppression and terminal status must agree: withholding the compaction item while
|
|
1174
|
+
// still reporting success hands codex-rs a completed response with zero compaction
|
|
1175
|
+
// items, which it treats as fatal.
|
|
1176
|
+
if (truncationReasonFor(event.stopReason)) {
|
|
1168
1177
|
// Upstream stopped before a normal completion. Surface as incomplete so the
|
|
1169
1178
|
// client can distinguish a truncated/filtered turn from a finished one.
|
|
1170
1179
|
const response = {
|
|
1171
1180
|
...responseSnapshot("incomplete", finishedItems, event.endTurn),
|
|
1172
1181
|
usage: responsesUsage(event.usage),
|
|
1173
1182
|
incomplete_details: {
|
|
1174
|
-
reason: event.stopReason
|
|
1183
|
+
reason: truncationReasonFor(event.stopReason) ?? "content_filter",
|
|
1175
1184
|
},
|
|
1176
1185
|
};
|
|
1177
1186
|
// Cache max-output partials so previous_response_id replay can continue them;
|
|
@@ -1467,7 +1476,15 @@ function buildResponseJSONWithBudget(
|
|
|
1467
1476
|
let incompleteEvent: Extract<AdapterEvent, { type: "incomplete" }> | undefined;
|
|
1468
1477
|
let endTurn: boolean | undefined;
|
|
1469
1478
|
let stopReason: string | undefined;
|
|
1479
|
+
// The adapter's stop reason exactly as it arrived. `stopReason` above is deliberately narrowed
|
|
1480
|
+
// to the two reasons that map onto a Responses `incomplete_details`; the raw value is what the
|
|
1481
|
+
// truncation guard needs, because adapters disagree on vocabulary (`length`, `refusal`, ...).
|
|
1482
|
+
let rawStopReason: string | undefined;
|
|
1470
1483
|
let cleanDone = false;
|
|
1484
|
+
// Whether the adapter emitted ANY terminal (done/error/incomplete). Distinct from `cleanDone`,
|
|
1485
|
+
// which is only true for a `done` without a stop reason. A buffered turn whose adapter simply
|
|
1486
|
+
// stopped emitting has no terminal at all, and must not be reported as a success.
|
|
1487
|
+
let sawTerminal = false;
|
|
1471
1488
|
let compactionText = "";
|
|
1472
1489
|
let compactionTextBytes = 0;
|
|
1473
1490
|
|
|
@@ -1782,20 +1799,30 @@ function buildResponseJSONWithBudget(
|
|
|
1782
1799
|
break;
|
|
1783
1800
|
case "error":
|
|
1784
1801
|
errorEvent = e;
|
|
1802
|
+
sawTerminal = true;
|
|
1785
1803
|
usage = e.usage ?? usage;
|
|
1786
1804
|
break;
|
|
1787
1805
|
case "incomplete":
|
|
1788
1806
|
incompleteEvent = e;
|
|
1807
|
+
sawTerminal = true;
|
|
1789
1808
|
endTurn = e.endTurn;
|
|
1790
1809
|
if (e.providerState) options?.onProviderState?.(e.providerState);
|
|
1791
1810
|
break;
|
|
1792
1811
|
case "done":
|
|
1793
1812
|
usage = e.usage;
|
|
1813
|
+
sawTerminal = true;
|
|
1794
1814
|
endTurn = e.endTurn;
|
|
1795
1815
|
cleanDone = e.stopReason === undefined;
|
|
1816
|
+
rawStopReason = e.stopReason;
|
|
1796
1817
|
if (e.providerState) options?.onProviderState?.(e.providerState);
|
|
1797
1818
|
// Match streaming: max_tokens and content_filter both terminate as incomplete.
|
|
1798
|
-
|
|
1819
|
+
// Normalize every adapter's truncation vocabulary to the canonical pair, so a raw
|
|
1820
|
+
// `length` or `refusal` reaches the status/incomplete_details logic below instead of
|
|
1821
|
+
// silently reading as a clean stop.
|
|
1822
|
+
{
|
|
1823
|
+
const truncation = truncationReasonFor(e.stopReason);
|
|
1824
|
+
if (truncation) stopReason = truncation === "max_output_tokens" ? "max_tokens" : "content_filter";
|
|
1825
|
+
}
|
|
1799
1826
|
break;
|
|
1800
1827
|
}
|
|
1801
1828
|
if (budget) releaseTranslatedEvent(e, budget);
|
|
@@ -1803,8 +1830,11 @@ function buildResponseJSONWithBudget(
|
|
|
1803
1830
|
flushText(cleanDone && !errorEvent && !incompleteEvent ? "final_answer" : undefined);
|
|
1804
1831
|
flushSummaryReasoning();
|
|
1805
1832
|
flushRawReasoning();
|
|
1806
|
-
// Open tool call on a failed/incomplete turn must not land as status:"completed"
|
|
1807
|
-
|
|
1833
|
+
// Open tool call on a failed/incomplete turn must not land as status:"completed" — and neither
|
|
1834
|
+
// must one left open by a stream that stopped without any terminal at all. That case previously
|
|
1835
|
+
// fell through to "completed", handing back a function_call whose arguments were half-written
|
|
1836
|
+
// JSON, inside a turn also marked completed.
|
|
1837
|
+
if (currentToolCallId) flushToolCall(errorEvent || incompleteEvent || !sawTerminal ? "incomplete" : "completed");
|
|
1808
1838
|
if (batchKiroRedacted) {
|
|
1809
1839
|
// pushOutput reserves the item itself and releases the retained raw blob it replaces.
|
|
1810
1840
|
pushOutput({
|
|
@@ -1820,8 +1850,12 @@ function buildResponseJSONWithBudget(
|
|
|
1820
1850
|
options?.compaction
|
|
1821
1851
|
&& !errorEvent
|
|
1822
1852
|
&& !incompleteEvent
|
|
1823
|
-
|
|
1824
|
-
|
|
1853
|
+
// A stream that stopped without any terminal did not complete either. The original guard
|
|
1854
|
+
// could only see explicit failure events, so an adapter EOF slipped past it and installed a
|
|
1855
|
+
// truncated summary as replacement history — the exact #422 hazard, reached by a route that
|
|
1856
|
+
// did not exist when the guard was written.
|
|
1857
|
+
&& sawTerminal
|
|
1858
|
+
&& !isTruncatedStopReason(rawStopReason)
|
|
1825
1859
|
) {
|
|
1826
1860
|
pushOutput({ type: "compaction", id: `cmp_${uuid()}`, encrypted_content: encodeCompactionSummary(compactionText) }, compactionTextBytes);
|
|
1827
1861
|
}
|
|
@@ -1831,7 +1865,13 @@ function buildResponseJSONWithBudget(
|
|
|
1831
1865
|
? "failed"
|
|
1832
1866
|
: incompleteEvent || stopReason === "max_tokens" || stopReason === "content_filter"
|
|
1833
1867
|
? "incomplete"
|
|
1834
|
-
:
|
|
1868
|
+
: sawTerminal
|
|
1869
|
+
? "completed"
|
|
1870
|
+
// The adapter stopped emitting without any terminal, so the turn was cut short. Streaming
|
|
1871
|
+
// already reports this as response.incomplete / adapter_eof (see the !terminated branch);
|
|
1872
|
+
// defaulting the buffered path to "completed" handed callers a truncated turn — including
|
|
1873
|
+
// one carrying a never-closed tool call with half-written JSON arguments — as a success.
|
|
1874
|
+
: "incomplete";
|
|
1835
1875
|
options?.onUsage?.(incompleteEvent?.usage ?? usage);
|
|
1836
1876
|
return {
|
|
1837
1877
|
id: responseId, object: "response",
|
|
@@ -1851,6 +1891,10 @@ function buildResponseJSONWithBudget(
|
|
|
1851
1891
|
incomplete_details: { reason: "max_output_tokens" },
|
|
1852
1892
|
} : stopReason === "content_filter" ? {
|
|
1853
1893
|
incomplete_details: { reason: "content_filter" },
|
|
1894
|
+
} : !sawTerminal ? {
|
|
1895
|
+
// Same reason string the streaming path uses, so a caller sees one signal for one condition
|
|
1896
|
+
// regardless of which surface it asked for.
|
|
1897
|
+
incomplete_details: { reason: "adapter_eof" },
|
|
1854
1898
|
} : {}),
|
|
1855
1899
|
usage: responsesUsage(incompleteEvent?.usage ?? usage),
|
|
1856
1900
|
};
|
|
@@ -348,8 +348,34 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
|
|
|
348
348
|
* Exported for the Windows integration regression that exercises the real
|
|
349
349
|
* PowerShell enumeration.
|
|
350
350
|
*/
|
|
351
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Turn one PowerShell enumeration's stdout into snapshots.
|
|
353
|
+
*
|
|
354
|
+
* Split out from the spawn so the failure contract is testable off-Windows: the
|
|
355
|
+
* sentinel path is the difference between "no Codex process is running" and "we could
|
|
356
|
+
* not read the process list", and only one of those is safe to act on.
|
|
357
|
+
*/
|
|
358
|
+
export function parseWindowsSnapshotOutput(output: string): ProcessSnapshot[] {
|
|
352
359
|
const out: ProcessSnapshot[] = [];
|
|
360
|
+
for (const line of output.split(/\r?\n/)) {
|
|
361
|
+
// A candidate whose owner could not be verified — or a top-level query that
|
|
362
|
+
// failed outright — makes the whole enumeration incomplete. The staleness
|
|
363
|
+
// collector must not read the partial result as "nothing running".
|
|
364
|
+
if (line.trim() === "__OCX_ENUM_INCOMPLETE__") throw new Error("windows_enum_incomplete");
|
|
365
|
+
const tab = line.indexOf("\t");
|
|
366
|
+
if (tab <= 0) continue;
|
|
367
|
+
const tab2 = line.indexOf("\t", tab + 1);
|
|
368
|
+
if (tab2 <= tab) continue;
|
|
369
|
+
const pid = Number(line.slice(0, tab));
|
|
370
|
+
const commandLine = line.slice(tab + 1, tab2).trim();
|
|
371
|
+
const owner = line.slice(tab2 + 1).trim();
|
|
372
|
+
if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine || !owner) continue;
|
|
373
|
+
out.push({ pid, commandLine, owner });
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function listWindowsSnapshots(runPowerShell?: (psCommand: string) => string): ProcessSnapshot[] {
|
|
353
379
|
// Newlines keep -Command as a real script (space-joined statements need ';').
|
|
354
380
|
// Double-quoted format string so `t expands to a real tab.
|
|
355
381
|
// Codex candidates only: basename token codex / codex.exe / codex.cmd /
|
|
@@ -361,7 +387,15 @@ export function listWindowsSnapshots(): ProcessSnapshot[] {
|
|
|
361
387
|
const psCommand = [
|
|
362
388
|
"$ErrorActionPreference='SilentlyContinue'",
|
|
363
389
|
"$me=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name",
|
|
364
|
-
|
|
390
|
+
// -ErrorAction Stop plus the outer try is what makes a TOP-LEVEL query failure
|
|
391
|
+
// observable. Under SilentlyContinue alone, a failing Get-CimInstance emits nothing
|
|
392
|
+
// and the enumeration is indistinguishable from "no Codex process is running" —
|
|
393
|
+
// the parse loop finds no rows, no sentinel is produced, and the staleness collector
|
|
394
|
+
// reports not_running for a machine whose process list it never actually read.
|
|
395
|
+
// The per-process catch below cannot cover this: it only runs once the pipeline has
|
|
396
|
+
// objects to iterate.
|
|
397
|
+
"try {",
|
|
398
|
+
"Get-CimInstance Win32_Process -ErrorAction Stop | Where-Object {",
|
|
365
399
|
" -not [string]::IsNullOrWhiteSpace($_.CommandLine) -and (",
|
|
366
400
|
` $_.CommandLine -match ${basenameMatch} -or`,
|
|
367
401
|
` $_.CommandLine -match ${codeModeMatch}`,
|
|
@@ -376,31 +410,19 @@ export function listWindowsSnapshots(): ProcessSnapshot[] {
|
|
|
376
410
|
" \"{0}`t{1}`t{2}\" -f $_.ProcessId, $cmd, $owner",
|
|
377
411
|
" } catch { \"__OCX_ENUM_INCOMPLETE__\" }",
|
|
378
412
|
"}",
|
|
413
|
+
"} catch { \"__OCX_ENUM_INCOMPLETE__\" }",
|
|
379
414
|
].join("\n");
|
|
380
415
|
// Top-level exec failure propagates (see listDarwinSnapshots note). The
|
|
381
416
|
// executable resolves from the trusted System32 directory (never PATH), and
|
|
382
417
|
// windowsHide keeps the enumeration console-less on desktop sessions (#1278).
|
|
383
|
-
const output =
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
// partial result as "nothing running".
|
|
392
|
-
if (line.trim() === "__OCX_ENUM_INCOMPLETE__") throw new Error("windows_enum_incomplete");
|
|
393
|
-
const tab = line.indexOf("\t");
|
|
394
|
-
if (tab <= 0) continue;
|
|
395
|
-
const tab2 = line.indexOf("\t", tab + 1);
|
|
396
|
-
if (tab2 <= tab) continue;
|
|
397
|
-
const pid = Number(line.slice(0, tab));
|
|
398
|
-
const commandLine = line.slice(tab + 1, tab2).trim();
|
|
399
|
-
const owner = line.slice(tab2 + 1).trim();
|
|
400
|
-
if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine || !owner) continue;
|
|
401
|
-
out.push({ pid, commandLine, owner });
|
|
402
|
-
}
|
|
403
|
-
return out;
|
|
418
|
+
const output = runPowerShell
|
|
419
|
+
? runPowerShell(psCommand)
|
|
420
|
+
: execFileSync(resolveTrustedWindowsPowerShellExe(), [
|
|
421
|
+
"-NoProfile", "-NoLogo", "-NonInteractive",
|
|
422
|
+
"-Command",
|
|
423
|
+
psCommand,
|
|
424
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true });
|
|
425
|
+
return parseWindowsSnapshotOutput(output);
|
|
404
426
|
}
|
|
405
427
|
|
|
406
428
|
function defaultListSnapshots(platform: NodeJS.Platform, getuid: () => number | undefined): ProcessSnapshot[] {
|
|
@@ -592,6 +614,18 @@ function defaultCatalogMtimeMs(): number | null {
|
|
|
592
614
|
// guidance calls (#857).
|
|
593
615
|
let catalogStateCache: { atMs: number; status: CodexAppServerCatalogStatus } | null = null;
|
|
594
616
|
const CATALOG_STATE_TTL_MS = 5_000;
|
|
617
|
+
/**
|
|
618
|
+
* `unknown` is a failure to observe, not an observation, so it gets a much shorter
|
|
619
|
+
* window than a real reading. At the full 5s a single transient enumeration failure
|
|
620
|
+
* suppresses guidance for every call in that window, and the retry that would have
|
|
621
|
+
* succeeded never runs. Keeping a brief window still collapses a burst of per-turn
|
|
622
|
+
* calls into one probe, which is what the cache is for.
|
|
623
|
+
*/
|
|
624
|
+
const CATALOG_STATE_UNKNOWN_TTL_MS = 250;
|
|
625
|
+
|
|
626
|
+
export function catalogStateTtlMs(state: CodexAppServerCatalogState): number {
|
|
627
|
+
return state === "unknown" ? CATALOG_STATE_UNKNOWN_TTL_MS : CATALOG_STATE_TTL_MS;
|
|
628
|
+
}
|
|
595
629
|
|
|
596
630
|
/**
|
|
597
631
|
* Compare the on-disk catalog mtime against the start time of running Codex
|
|
@@ -616,7 +650,8 @@ export function collectCodexAppServerCatalogState(
|
|
|
616
650
|
const fullyDefault = !io.listSnapshots && !io.readStartMs && !io.catalogMtimeMs
|
|
617
651
|
&& !io.platform && !io.getuid && !io.now;
|
|
618
652
|
if (fullyDefault
|
|
619
|
-
&& catalogStateCache
|
|
653
|
+
&& catalogStateCache
|
|
654
|
+
&& now - catalogStateCache.atMs < catalogStateTtlMs(catalogStateCache.status.state)) {
|
|
620
655
|
return catalogStateCache.status;
|
|
621
656
|
}
|
|
622
657
|
const compute = (): CodexAppServerCatalogStatus => {
|
|
@@ -630,17 +665,16 @@ export function collectCodexAppServerCatalogState(
|
|
|
630
665
|
});
|
|
631
666
|
let snapshots: ProcessSnapshot[];
|
|
632
667
|
let enumerationFailed = false;
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
668
|
+
const enumerate = io.listSnapshots ?? (() => defaultListSnapshots(platform, getuid));
|
|
669
|
+
try {
|
|
670
|
+
snapshots = enumerate();
|
|
671
|
+
} catch {
|
|
672
|
+
// Enumeration failure must never read as "nothing running" — that would let
|
|
673
|
+
// positive model guidance through on guesswork (#857). The injected seam gets
|
|
674
|
+
// the same contract as the default path: whoever enumerates, a failure to read
|
|
675
|
+
// the process list is unknown, not an empty machine.
|
|
676
|
+
snapshots = [];
|
|
677
|
+
enumerationFailed = true;
|
|
644
678
|
}
|
|
645
679
|
const processes: CodexAppServerProcess[] = [];
|
|
646
680
|
const seen = new Set<number>();
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type CapturedServiceTierAdapterAuthority,
|
|
39
39
|
} from "../../providers/service-tier";
|
|
40
40
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
41
|
-
import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
|
|
41
|
+
import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
|
|
42
42
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
43
43
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
44
44
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
@@ -1374,6 +1374,10 @@ async function fetchProviderModelsWithAuth(
|
|
|
1374
1374
|
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
1375
1375
|
return observed(withConfiguredRetention(configured), "degraded");
|
|
1376
1376
|
}
|
|
1377
|
+
registerAntigravityDiscoveredWireModels(prov.baseUrl, antigravity, {
|
|
1378
|
+
provider: name,
|
|
1379
|
+
cacheGeneration,
|
|
1380
|
+
});
|
|
1377
1381
|
markProviderDiscoveryOk(name, live.length);
|
|
1378
1382
|
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
|
|
1379
1383
|
}
|
package/src/config.ts
CHANGED
|
@@ -741,6 +741,7 @@ const providerConfigSchema = z.object({
|
|
|
741
741
|
upstreamHttpVersion: z.enum(UPSTREAM_HTTP_VERSION_VALUES)
|
|
742
742
|
.nullish()
|
|
743
743
|
.transform(value => value ?? undefined),
|
|
744
|
+
directGeminiWireRenames: z.boolean().optional(),
|
|
744
745
|
noStructuredOutputModels: z.array(z.string().min(1))
|
|
745
746
|
.transform(normalizeNonBlankStringArray)
|
|
746
747
|
.optional(),
|