@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
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
sseDataPayload,
|
|
25
25
|
type SseBlockRewrite,
|
|
26
26
|
} from "../sse-payload-rewrite";
|
|
27
|
+
import { isCompactionItemType } from "../../responses/compaction";
|
|
27
28
|
|
|
28
29
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
29
30
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -119,16 +120,6 @@ function backfillContentArray(content: unknown): unknown {
|
|
|
119
120
|
return changed ? repaired : content;
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
/**
|
|
123
|
-
* Item types that are NOT Responses output items and must be returned byte-for-byte.
|
|
124
|
-
*
|
|
125
|
-
* `compaction` is the `/v1/responses/compact` wire format, not a Responses output item. It has
|
|
126
|
-
* no `id` in that contract, so synthesizing one changes a response body the client compares
|
|
127
|
-
* exactly. The backfill exists to satisfy strict Responses decoders; a shape those decoders
|
|
128
|
-
* never see is outside its remit.
|
|
129
|
-
*/
|
|
130
|
-
const NON_RESPONSES_ITEM_TYPES: ReadonlySet<string> = new Set(["compaction"]);
|
|
131
|
-
|
|
132
123
|
/**
|
|
133
124
|
* Walk an output item and backfill output_text parts in its content.
|
|
134
125
|
* Also backfills a missing required id on the item itself.
|
|
@@ -136,7 +127,12 @@ const NON_RESPONSES_ITEM_TYPES: ReadonlySet<string> = new Set(["compaction"]);
|
|
|
136
127
|
*/
|
|
137
128
|
function backfillOutputItem(item: unknown, slot: ItemIdSlot): unknown {
|
|
138
129
|
if (!isPlainObject(item)) return item;
|
|
139
|
-
|
|
130
|
+
// The compact wire family is the `/v1/responses/compact` format, not a Responses output item.
|
|
131
|
+
// Those items have no `id` in that contract, so synthesizing one changes a response body the
|
|
132
|
+
// client compares exactly — and the client replays the item on every later turn, where the
|
|
133
|
+
// minting backend rejects it as modified. The backfill exists to satisfy strict Responses
|
|
134
|
+
// decoders; a shape those decoders never see is outside its remit.
|
|
135
|
+
if (isCompactionItemType(item.type)) return item;
|
|
140
136
|
const content = item.content;
|
|
141
137
|
const repaired = backfillContentArray(content);
|
|
142
138
|
const withId = backfillItemId(item, slot);
|
|
@@ -117,7 +117,7 @@ export function analyzeTerminalTurn(parsed: OcxParsedRequest, events: readonly A
|
|
|
117
117
|
return { decision: "continue", reason: "suspicious_no_tool", assistantText: text, userText, hasToolCall };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
function assistantMessageFromEvents(events: readonly AdapterEvent[]): OcxAssistantMessage | undefined {
|
|
120
|
+
function assistantMessageFromEvents(events: readonly AdapterEvent[], timestamp: number): OcxAssistantMessage | undefined {
|
|
121
121
|
let text = "";
|
|
122
122
|
let thinking = "";
|
|
123
123
|
let signature: string | undefined;
|
|
@@ -134,14 +134,18 @@ function assistantMessageFromEvents(events: readonly AdapterEvent[]): OcxAssista
|
|
|
134
134
|
}
|
|
135
135
|
if (text) content.push({ type: "text", text });
|
|
136
136
|
if (content.length === 0) return undefined;
|
|
137
|
-
return { role: "assistant", content, timestamp
|
|
137
|
+
return { role: "assistant", content, timestamp };
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
export function buildContinuationRequest(parsed: OcxParsedRequest, events: readonly AdapterEvent[]): OcxParsedRequest {
|
|
141
141
|
const messages = [...parsed.context.messages];
|
|
142
|
-
|
|
142
|
+
// One clock read for the whole rebuild. Two Date.now() calls could straddle a millisecond
|
|
143
|
+
// boundary, which made the retained-heartbeat contract test fail intermittently in CI on a
|
|
144
|
+
// 1ms skew between the assistant message and the nudge that follows it.
|
|
145
|
+
const timestamp = Date.now();
|
|
146
|
+
const assistant = assistantMessageFromEvents(events, timestamp);
|
|
143
147
|
if (assistant) messages.push(assistant);
|
|
144
|
-
messages.push({ role: "developer", content: TERMINAL_GUARD_NUDGE, timestamp
|
|
148
|
+
messages.push({ role: "developer", content: TERMINAL_GUARD_NUDGE, timestamp });
|
|
145
149
|
return { ...parsed, context: { ...parsed.context, messages } };
|
|
146
150
|
}
|
|
147
151
|
|
|
@@ -153,6 +157,16 @@ export interface GuardedEventStreamOptions {
|
|
|
153
157
|
maxAutoContinuations?: number;
|
|
154
158
|
}
|
|
155
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Events that are useful to the downstream stream consumer but carry no state used by the
|
|
162
|
+
* terminal-continuation decision or its rebuilt request. Keep them out of the retained event
|
|
163
|
+
* list so adapter liveness markers and arbitrarily large tool-argument fragments cannot make
|
|
164
|
+
* the guard's per-turn memory grow without adding any continuation semantics.
|
|
165
|
+
*/
|
|
166
|
+
export function isTerminalGuardPassthroughOnly(event: AdapterEvent): boolean {
|
|
167
|
+
return event.type === "heartbeat" || event.type === "tool_call_delta";
|
|
168
|
+
}
|
|
169
|
+
|
|
156
170
|
function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undefined): OcxUsage | undefined {
|
|
157
171
|
if (!first) return second;
|
|
158
172
|
if (!second) return first;
|
|
@@ -193,13 +207,10 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
|
|
|
193
207
|
const seen: AdapterEvent[] = [];
|
|
194
208
|
let terminalSeen = false;
|
|
195
209
|
for await (const event of source) {
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
// tool-call delta, so a long argument payload alone could grow this array without
|
|
201
|
-
// limit. The empty-completion guard already passes them through unretained; match it.
|
|
202
|
-
if (event.type === "heartbeat") {
|
|
210
|
+
// Liveness markers and tool argument fragments are passed through to the bridge, but
|
|
211
|
+
// neither analyzeTerminalTurn nor buildContinuationRequest consumes them. Retaining the
|
|
212
|
+
// fragments would duplicate arbitrarily large argument payloads in `seen` for no effect.
|
|
213
|
+
if (isTerminalGuardPassthroughOnly(event)) {
|
|
203
214
|
yield event;
|
|
204
215
|
continue;
|
|
205
216
|
}
|
|
@@ -2,6 +2,7 @@ import type { TranslatorBudget } from "../lib/translator-budget";
|
|
|
2
2
|
import {
|
|
3
3
|
customToolItemId,
|
|
4
4
|
restoreRoutedCustomCalls,
|
|
5
|
+
routedCustomToolWireName,
|
|
5
6
|
unwrapRoutedCustomToolArguments,
|
|
6
7
|
} from "../responses/custom-tool-compat";
|
|
7
8
|
import {
|
|
@@ -181,7 +182,8 @@ export function createRoutedCustomToolRestoreBlockRewrite(
|
|
|
181
182
|
&& typeof parsed.item.name === "string"
|
|
182
183
|
) {
|
|
183
184
|
const upstreamItemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined;
|
|
184
|
-
const
|
|
185
|
+
const wireName = routedCustomToolWireName(parsed.item);
|
|
186
|
+
const routed = wireName !== undefined && names.has(wireName);
|
|
185
187
|
if (upstreamItemId) {
|
|
186
188
|
if (routed) {
|
|
187
189
|
itemNames.set(upstreamItemId, parsed.item.name);
|
|
@@ -34,6 +34,13 @@ function reasoningTextOf(item: Record<string, unknown>): string {
|
|
|
34
34
|
/** Move a reasoning item's content channel into the summary channel. */
|
|
35
35
|
function reasoningItemToSummaryShape(item: Record<string, unknown>): Record<string, unknown> {
|
|
36
36
|
if (item.type !== "reasoning") return item;
|
|
37
|
+
// `encrypted_content` is opaque, state-bearing provider data, so the entire item must retain its
|
|
38
|
+
// upstream shape unless that backend has an explicit replay contract permitting a rewrite. This
|
|
39
|
+
// defensively protects content-channel backends that do issue blobs when the client replays the
|
|
40
|
+
// stored item. The delta rewrite can still provide the expandable trace for the live turn.
|
|
41
|
+
// DeepSeek — the provider this rewrite was verified against — is `statelessResponses` and issues
|
|
42
|
+
// no blob, so it is unaffected.
|
|
43
|
+
if (typeof item.encrypted_content === "string" && item.encrypted_content.length > 0) return item;
|
|
37
44
|
const text = reasoningTextOf(item);
|
|
38
45
|
// Items that already use the summary channel (or carry no content text at
|
|
39
46
|
// all) are left untouched: rewriting them could clear a valid summary.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isTranslatorBudgetExceededError,
|
|
3
|
+
TranslatorBudgetExceededError,
|
|
3
4
|
type TranslatorBudget,
|
|
4
5
|
} from "../lib/translator-budget";
|
|
5
6
|
import {
|
|
@@ -24,6 +25,19 @@ type PendingArgumentBlock = {
|
|
|
24
25
|
|
|
25
26
|
const MAX_PENDING_ARGUMENT_FRAMES = 256;
|
|
26
27
|
const MAX_PENDING_ARGUMENT_BYTES = 1024 * 1024;
|
|
28
|
+
const MAX_CLASSIFIED_ITEM_IDS = 256;
|
|
29
|
+
const MAX_CLASSIFIED_ITEM_ID_BYTES = 256 * 1024;
|
|
30
|
+
|
|
31
|
+
class ClassifiedItemIdCountExceededError extends Error {
|
|
32
|
+
readonly code = "translation_buffer_limit";
|
|
33
|
+
readonly kind = "item_ids";
|
|
34
|
+
readonly limitItems = MAX_CLASSIFIED_ITEM_IDS;
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
super(`translator item_ids count exceeded ${MAX_CLASSIFIED_ITEM_IDS} items`);
|
|
38
|
+
this.name = "ClassifiedItemIdCountExceededError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
27
41
|
|
|
28
42
|
/**
|
|
29
43
|
* Public Responses gateways stream a lowered search as a normal function lifecycle. Codex expects
|
|
@@ -36,6 +50,7 @@ export function createRoutedToolSearchRestoreBlockRewrite(
|
|
|
36
50
|
): SseBlockRewrite {
|
|
37
51
|
const routedItemIds = new Set<string>();
|
|
38
52
|
const ordinaryItemIds = new Set<string>();
|
|
53
|
+
let classifiedItemIdBytes = 0;
|
|
39
54
|
let pendingArguments: PendingArgumentBlock[] = [];
|
|
40
55
|
let pendingArgumentBytes = 0;
|
|
41
56
|
let passthrough = false;
|
|
@@ -49,10 +64,45 @@ export function createRoutedToolSearchRestoreBlockRewrite(
|
|
|
49
64
|
}
|
|
50
65
|
pendingArguments = [];
|
|
51
66
|
pendingArgumentBytes = 0;
|
|
67
|
+
if (classifiedItemIdBytes > 0) {
|
|
68
|
+
budget?.releaseRetained(classifiedItemIdBytes, { kind: "item_ids" });
|
|
69
|
+
}
|
|
70
|
+
classifiedItemIdBytes = 0;
|
|
52
71
|
routedItemIds.clear();
|
|
53
72
|
ordinaryItemIds.clear();
|
|
54
73
|
};
|
|
55
74
|
|
|
75
|
+
const clearOrdinaryItemIds = (): void => {
|
|
76
|
+
let releasedBytes = 0;
|
|
77
|
+
for (const itemId of ordinaryItemIds) {
|
|
78
|
+
releasedBytes += Buffer.byteLength(JSON.stringify(itemId), "utf8");
|
|
79
|
+
}
|
|
80
|
+
ordinaryItemIds.clear();
|
|
81
|
+
classifiedItemIdBytes = Math.max(0, classifiedItemIdBytes - releasedBytes);
|
|
82
|
+
if (releasedBytes > 0) budget?.releaseRetained(releasedBytes, { kind: "item_ids" });
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const classifyItemId = (itemId: string, routed: boolean): void => {
|
|
86
|
+
const target = routed ? routedItemIds : ordinaryItemIds;
|
|
87
|
+
const previous = routed ? ordinaryItemIds : routedItemIds;
|
|
88
|
+
if (target.has(itemId)) return;
|
|
89
|
+
if (previous.delete(itemId)) {
|
|
90
|
+
target.add(itemId);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (routedItemIds.size + ordinaryItemIds.size >= MAX_CLASSIFIED_ITEM_IDS) {
|
|
95
|
+
throw new ClassifiedItemIdCountExceededError();
|
|
96
|
+
}
|
|
97
|
+
const retainedBytes = Buffer.byteLength(JSON.stringify(itemId), "utf8");
|
|
98
|
+
if (classifiedItemIdBytes + retainedBytes > MAX_CLASSIFIED_ITEM_ID_BYTES) {
|
|
99
|
+
throw new TranslatorBudgetExceededError("item_ids", MAX_CLASSIFIED_ITEM_ID_BYTES);
|
|
100
|
+
}
|
|
101
|
+
budget?.chargeRetained(retainedBytes, { kind: "item_ids" });
|
|
102
|
+
target.add(itemId);
|
|
103
|
+
classifiedItemIdBytes += retainedBytes;
|
|
104
|
+
};
|
|
105
|
+
|
|
56
106
|
const retainPending = (
|
|
57
107
|
block: string,
|
|
58
108
|
itemId: string | undefined,
|
|
@@ -73,7 +123,7 @@ export function createRoutedToolSearchRestoreBlockRewrite(
|
|
|
73
123
|
// that we forget what we already classified: an item restored to `tool_search_call`
|
|
74
124
|
// upstream of here would otherwise start emitting `function_call_arguments.*` again and
|
|
75
125
|
// the client would see a mixed private/public lifecycle for one call.
|
|
76
|
-
|
|
126
|
+
clearOrdinaryItemIds();
|
|
77
127
|
return flushed;
|
|
78
128
|
}
|
|
79
129
|
if (retainedBytes > 0) {
|
|
@@ -90,7 +140,7 @@ export function createRoutedToolSearchRestoreBlockRewrite(
|
|
|
90
140
|
passthrough = true;
|
|
91
141
|
// Same reasoning as the frame/byte overflow above: an already-restored routed item
|
|
92
142
|
// must keep its frames suppressed even once buffering stops.
|
|
93
|
-
|
|
143
|
+
clearOrdinaryItemIds();
|
|
94
144
|
return flushed;
|
|
95
145
|
}
|
|
96
146
|
}
|
|
@@ -135,7 +185,11 @@ export function createRoutedToolSearchRestoreBlockRewrite(
|
|
|
135
185
|
const rewrite: SseBlockRewrite = (block: string): readonly string[] => {
|
|
136
186
|
if (disposed) return [block];
|
|
137
187
|
const payload = sseDataPayload(block);
|
|
138
|
-
if (payload === null
|
|
188
|
+
if (payload === null) return [block];
|
|
189
|
+
if (payload === "[DONE]") {
|
|
190
|
+
releaseAll();
|
|
191
|
+
return [block];
|
|
192
|
+
}
|
|
139
193
|
let parsed: unknown;
|
|
140
194
|
try {
|
|
141
195
|
parsed = JSON.parse(payload);
|
|
@@ -145,11 +199,16 @@ export function createRoutedToolSearchRestoreBlockRewrite(
|
|
|
145
199
|
if (!isPlainObject(parsed)) return [block];
|
|
146
200
|
|
|
147
201
|
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
202
|
+
const terminal = type === "response.completed" || type === "response.failed" || type === "response.incomplete";
|
|
148
203
|
// After overflow we stop BUFFERING unknown frames, but an item already restored to
|
|
149
204
|
// `tool_search_call` must keep its public argument frames suppressed — otherwise the client
|
|
150
205
|
// receives a private item followed by `function_call_arguments.*` for the same id, which is
|
|
151
206
|
// exactly the mixed lifecycle this rewrite exists to prevent. Everything else passes through.
|
|
152
207
|
if (passthrough) {
|
|
208
|
+
if (terminal) {
|
|
209
|
+
releaseAll();
|
|
210
|
+
return [block];
|
|
211
|
+
}
|
|
153
212
|
const passthroughItemId = typeof parsed.item_id === "string" ? parsed.item_id : undefined;
|
|
154
213
|
const isArgumentEvent = type === "response.function_call_arguments.delta"
|
|
155
214
|
|| type === "response.function_call_arguments.done";
|
|
@@ -169,22 +228,14 @@ export function createRoutedToolSearchRestoreBlockRewrite(
|
|
|
169
228
|
) {
|
|
170
229
|
const itemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined;
|
|
171
230
|
const routed = names.has(parsed.item.name);
|
|
172
|
-
if (itemId)
|
|
173
|
-
if (routed) {
|
|
174
|
-
routedItemIds.add(itemId);
|
|
175
|
-
ordinaryItemIds.delete(itemId);
|
|
176
|
-
} else {
|
|
177
|
-
ordinaryItemIds.add(itemId);
|
|
178
|
-
routedItemIds.delete(itemId);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
231
|
+
if (itemId) classifyItemId(itemId, routed);
|
|
181
232
|
const pending = takePending(itemId, outputIndex);
|
|
182
233
|
const restored = routed ? restoreRoutedToolSearchCalls(parsed, names) : { value: parsed, changed: false };
|
|
183
234
|
const restoredBlock = restored.changed
|
|
184
235
|
? replaceSseDataPayload(block, JSON.stringify(restored.value))
|
|
185
236
|
: block;
|
|
186
237
|
// Classification is retained past `output_item.done` for BOTH kinds, until the terminal
|
|
187
|
-
// event releases
|
|
238
|
+
// event releases the bounded, budgeted state.
|
|
188
239
|
//
|
|
189
240
|
// `done` ends the item, not the id's relevance. Forgetting a ROUTED id let a trailing
|
|
190
241
|
// `function_call_arguments.*` — which some upstreams emit after done — fall through to
|
|
@@ -204,7 +255,6 @@ export function createRoutedToolSearchRestoreBlockRewrite(
|
|
|
204
255
|
}
|
|
205
256
|
if (argumentEvent && itemId && routedItemIds.has(itemId)) return [];
|
|
206
257
|
|
|
207
|
-
const terminal = type === "response.completed" || type === "response.failed" || type === "response.incomplete";
|
|
208
258
|
if (!terminal) return [block];
|
|
209
259
|
const restored = restoreRoutedToolSearchCalls(parsed, names);
|
|
210
260
|
releaseAll();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place that decides whether a sidecar may treat ChatGPT or Anthropic
|
|
3
|
+
* auth as PRESENT (#2188). Web-search and vision consumed two hand-rolled
|
|
4
|
+
* copies of the Anthropic predicate and no Codex-login predicate at all —
|
|
5
|
+
* provider presence was standing in for "logged in", which let a fresh install
|
|
6
|
+
* with the built-in forward provider but no credential offer Luna as a
|
|
7
|
+
* describer it could never run.
|
|
8
|
+
*
|
|
9
|
+
* Both flags are request-context-free: they read config plus the stored
|
|
10
|
+
* account state, never headers. Per-request usability (exact accounts,
|
|
11
|
+
* generation fences) stays in resolveFirstUsableOpenAiSidecar and the
|
|
12
|
+
* executors; this module only answers "is this side worth offering at all?".
|
|
13
|
+
*/
|
|
14
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
15
|
+
import { listOpenAiForwardSidecarCandidates } from "../providers/openai-sidecar";
|
|
16
|
+
import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
17
|
+
import { isCodexAccountUsable } from "../codex/account-usability";
|
|
18
|
+
import { MAIN_CODEX_ACCOUNT_ID, isSelectableCodexPoolAccount } from "../codex/account-id";
|
|
19
|
+
import { getAccountSet } from "../oauth/store";
|
|
20
|
+
|
|
21
|
+
export interface SidecarAuthState {
|
|
22
|
+
/** ChatGPT login usable: canonical forward provider AND a live stored credential. */
|
|
23
|
+
isCodexAuth: boolean;
|
|
24
|
+
/** Enabled anthropic-adapter OAuth provider whose active account is not marked for reauth. */
|
|
25
|
+
isAnthropicAuth: boolean;
|
|
26
|
+
/** The provider an Anthropic-side executor would dispatch through, when isAnthropicAuth. */
|
|
27
|
+
anthropicProviderName?: string;
|
|
28
|
+
anthropicProvider?: OcxProviderConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fixed auth-slot models (#2188): logged-in sides keep these candidates even
|
|
33
|
+
* when the picker hides or disables them. The slot is the LOGIN's entitlement,
|
|
34
|
+
* not the catalog's.
|
|
35
|
+
*/
|
|
36
|
+
export const AUTH_SLOT_MODELS = {
|
|
37
|
+
codex: "gpt-5.6-luna",
|
|
38
|
+
anthropic: "claude-haiku-4-5",
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
export interface SidecarAuthSlot {
|
|
42
|
+
provider: string;
|
|
43
|
+
id: string;
|
|
44
|
+
slot: keyof typeof AUTH_SLOT_MODELS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Login-shaped, not provider-shaped: a forward provider with no credential is NOT Codex auth. */
|
|
48
|
+
function hasUsableCodexLogin(config: OcxConfig): boolean {
|
|
49
|
+
if (listOpenAiForwardSidecarCandidates(config).length === 0) return false;
|
|
50
|
+
if (isCodexAccountUsable(config, MAIN_CODEX_ACCOUNT_ID)) return true;
|
|
51
|
+
return (config.codexAccounts ?? []).some(account =>
|
|
52
|
+
isSelectableCodexPoolAccount(account) && isCodexAccountUsable(config, account.id));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The predicate previously duplicated as findAnthropicSidecarProvider
|
|
57
|
+
* (web-search) and findAnthropicVisionProvider (vision): first enabled
|
|
58
|
+
* anthropic-adapter OAuth provider whose ACTIVE stored account holds a usable
|
|
59
|
+
* credential. getAccountSet + needsReauth, not getCredential — a terminally
|
|
60
|
+
* invalid account must not present as auth (audit F1).
|
|
61
|
+
*/
|
|
62
|
+
function findAnthropicAuthProvider(
|
|
63
|
+
config: OcxConfig,
|
|
64
|
+
): { providerName: string; provider: OcxProviderConfig } | undefined {
|
|
65
|
+
for (const [providerName, provider] of Object.entries(config.providers)) {
|
|
66
|
+
if (provider.disabled === true) continue;
|
|
67
|
+
if (provider.adapter !== "anthropic" || provider.authMode !== "oauth") continue;
|
|
68
|
+
const set = getAccountSet(providerName);
|
|
69
|
+
const active = set?.accounts.find(account => account.id === set.activeAccountId);
|
|
70
|
+
if (active && active.needsReauth !== true) return { providerName, provider };
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolveSidecarAuth(config: OcxConfig): SidecarAuthState {
|
|
76
|
+
const anthropic = findAnthropicAuthProvider(config);
|
|
77
|
+
return {
|
|
78
|
+
isCodexAuth: hasUsableCodexLogin(config),
|
|
79
|
+
isAnthropicAuth: anthropic !== undefined,
|
|
80
|
+
...(anthropic ? { anthropicProviderName: anthropic.providerName, anthropicProvider: anthropic.provider } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The auth-entitled fixed candidates. Emitted regardless of picker visibility. */
|
|
85
|
+
export function sidecarAuthSlots(auth: SidecarAuthState): SidecarAuthSlot[] {
|
|
86
|
+
const slots: SidecarAuthSlot[] = [];
|
|
87
|
+
if (auth.isCodexAuth) slots.push({ provider: OPENAI_CODEX_PROVIDER_ID, id: AUTH_SLOT_MODELS.codex, slot: "codex" });
|
|
88
|
+
if (auth.isAnthropicAuth && auth.anthropicProviderName) {
|
|
89
|
+
slots.push({ provider: auth.anthropicProviderName, id: AUTH_SLOT_MODELS.anthropic, slot: "anthropic" });
|
|
90
|
+
}
|
|
91
|
+
return slots;
|
|
92
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified picker-visibility candidate set both sidecars consume (#2188).
|
|
3
|
+
*
|
|
4
|
+
* Rule 1 (picker): a model may be OFFERED as a sidecar backend only if the
|
|
5
|
+
* management picker would show it — the same `listManagementModelRows` rows
|
|
6
|
+
* the GUI Models tab renders, minus disabled rows. Rule 1's only exception is
|
|
7
|
+
* the auth slots: a logged-in side keeps its fixed slot model (Luna/Haiku)
|
|
8
|
+
* even when the picker hides it, because the slot is the login's entitlement.
|
|
9
|
+
*
|
|
10
|
+
* Vision additionally applies rule 2 (− provably text-only); web-search
|
|
11
|
+
* applies its own rule 2 (∩ probed backend with an executor) in
|
|
12
|
+
* src/web-search/backends.ts. Both start from THIS set so the two sidecars
|
|
13
|
+
* cannot diverge on what "visible" means.
|
|
14
|
+
*/
|
|
15
|
+
import type { OcxConfig } from "../types";
|
|
16
|
+
import { listManagementModelRows } from "../server/management/model-rows";
|
|
17
|
+
import { modelAcceptsImageInput, type VisionCandidateModel } from "../vision/eligibility";
|
|
18
|
+
import { sidecarAuthSlots, type SidecarAuthState } from "./auth";
|
|
19
|
+
|
|
20
|
+
export interface SidecarCandidate {
|
|
21
|
+
provider: string;
|
|
22
|
+
id: string;
|
|
23
|
+
native?: boolean;
|
|
24
|
+
inputModalities?: string[];
|
|
25
|
+
/** True when this row is an auth-slot entitlement rather than a picker row. */
|
|
26
|
+
authSlot?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* (picker-visible rows) ∪ (auth slots). De-duplicated by provider+id with the
|
|
31
|
+
* auth-slot flag winning, so a slot model that is ALSO picker-visible still
|
|
32
|
+
* reads as slot-backed. A catalog outage degrades to slots only — the settings
|
|
33
|
+
* routes must not 500 and must keep the logged-in floor populated.
|
|
34
|
+
*/
|
|
35
|
+
export async function pickerVisibleSidecarCandidates(
|
|
36
|
+
config: OcxConfig,
|
|
37
|
+
auth: SidecarAuthState,
|
|
38
|
+
): Promise<SidecarCandidate[]> {
|
|
39
|
+
let rows: Awaited<ReturnType<typeof listManagementModelRows>> = [];
|
|
40
|
+
try { rows = await listManagementModelRows(config); } catch { rows = []; }
|
|
41
|
+
const byKey = new Map<string, SidecarCandidate>();
|
|
42
|
+
for (const row of rows) {
|
|
43
|
+
if (row.disabled === true) continue;
|
|
44
|
+
byKey.set(`${row.provider}/${row.id}`, {
|
|
45
|
+
provider: row.provider,
|
|
46
|
+
id: row.id,
|
|
47
|
+
...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
|
|
48
|
+
...(row.native ? { native: true } : {}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
for (const slot of sidecarAuthSlots(auth)) {
|
|
52
|
+
byKey.set(`${slot.provider}/${slot.id}`, {
|
|
53
|
+
provider: slot.provider,
|
|
54
|
+
id: slot.id,
|
|
55
|
+
// Slot models are known image-capable; their only exclusion path is the
|
|
56
|
+
// provider's explicit consumer list (same stance as baselineCandidate).
|
|
57
|
+
inputModalities: ["text", "image"],
|
|
58
|
+
authSlot: true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return [...byKey.values()];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Rule 2 for vision: drop rows PROVABLY text-only. Unknown stays eligible —
|
|
66
|
+
* the picker filter (rule 1) already bounded the set, so permissive-unknown
|
|
67
|
+
* no longer expands to the whole catalog.
|
|
68
|
+
*/
|
|
69
|
+
export function visionSidecarCandidates(
|
|
70
|
+
config: Pick<OcxConfig, "providers">,
|
|
71
|
+
all: readonly SidecarCandidate[],
|
|
72
|
+
): SidecarCandidate[] {
|
|
73
|
+
return all.filter(candidate => modelAcceptsImageInput(config, toVisionCandidate(candidate)) !== false);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toVisionCandidate(candidate: SidecarCandidate): VisionCandidateModel {
|
|
77
|
+
return {
|
|
78
|
+
provider: candidate.provider,
|
|
79
|
+
id: candidate.id,
|
|
80
|
+
...(candidate.inputModalities ? { inputModalities: candidate.inputModalities } : {}),
|
|
81
|
+
...(candidate.native ? { native: true } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -128,7 +128,7 @@ export interface OcxClaudeCodeConfig {
|
|
|
128
128
|
*/
|
|
129
129
|
subagentEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
|
130
130
|
/** Claude-originated web-search override. Unset fields inherit the global sidecar settings. */
|
|
131
|
-
webSearchSidecar?: { backend?: "openai" | "anthropic"; model?: string };
|
|
131
|
+
webSearchSidecar?: { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa"; model?: string };
|
|
132
132
|
/** Claude-originated vision override. Unset fields inherit the global sidecar settings. */
|
|
133
133
|
visionSidecar?: { backend?: "openai" | "anthropic"; model?: string };
|
|
134
134
|
/** Persisted Claude Desktop four-family routing profile. */
|
|
@@ -790,12 +790,33 @@ export interface OcxWebSearchSidecarConfig {
|
|
|
790
790
|
/**
|
|
791
791
|
* Which backend actually runs the server-side search. "openai" replays the hosted web_search via
|
|
792
792
|
* the ChatGPT forward provider (gpt-mini sidecar); "anthropic" runs web_search_20250305 on a Claude
|
|
793
|
-
* model authenticated by the STORED anthropic OAuth credential.
|
|
794
|
-
*
|
|
795
|
-
|
|
796
|
-
|
|
793
|
+
* model authenticated by the STORED anthropic OAuth credential. "xai" runs Grok hosted web_search
|
|
794
|
+
* and optional x_search through stored Grok OAuth. "gemini" (google_search grounding via the
|
|
795
|
+
* Antigravity CCA transport) and "exa" (non-LLM search JSON via an operator key) are explicit-only
|
|
796
|
+
* and stay inactive until their executors ship. Unset ALWAYS resolves to "openai"; no backend is ever
|
|
797
|
+
* auto-selected from credential availability (that once sent incompatible models to the
|
|
798
|
+
* Anthropic API — see resolveSidecarBackend).
|
|
799
|
+
*/
|
|
800
|
+
backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa";
|
|
797
801
|
/** Sidecar model that runs the real server-side web_search (must be a native ChatGPT model). */
|
|
798
802
|
model?: string;
|
|
803
|
+
/**
|
|
804
|
+
* Operator-supplied Exa API key for the "exa" backend. Management GET responses never echo it,
|
|
805
|
+
* and src/lib/redact.ts strips it from any logged structure or error string.
|
|
806
|
+
*/
|
|
807
|
+
exaApiKey?: string;
|
|
808
|
+
/**
|
|
809
|
+
* Opt-in X (Twitter) search for the xai backend: adds the hosted x_search tool next to
|
|
810
|
+
* web_search. Limits are doc-validated at the management layer AND in the executor:
|
|
811
|
+
* handles <=20 per list, allow XOR exclude, ISO-8601 dates.
|
|
812
|
+
*/
|
|
813
|
+
xSearch?: {
|
|
814
|
+
enabled?: boolean;
|
|
815
|
+
allowedXHandles?: string[];
|
|
816
|
+
excludedXHandles?: string[];
|
|
817
|
+
fromDate?: string;
|
|
818
|
+
toDate?: string;
|
|
819
|
+
};
|
|
799
820
|
/** Reasoning effort for the sidecar — "minimal" (non-thinking) keeps it fast/cheap. */
|
|
800
821
|
reasoning?: string;
|
|
801
822
|
/** Max searches executed per main-model turn (loop guard). */
|
package/src/types/provider.ts
CHANGED
|
@@ -204,6 +204,11 @@ export interface OcxProviderConfig {
|
|
|
204
204
|
* `ocxr1` envelopes are still stripped because no upstream can decrypt them.
|
|
205
205
|
*/
|
|
206
206
|
preserveResponsesReasoningContent?: boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Explicit opt-in for a relay that genuinely fronts OpenAI and can decode native
|
|
209
|
+
* compaction blobs. Absent or false degrades foreign blobs to an opaque note.
|
|
210
|
+
*/
|
|
211
|
+
decodesNativeCompactionBlobs?: boolean;
|
|
207
212
|
/**
|
|
208
213
|
* Explicit opt-in for non-registry private-network destinations such as localhost, RFC1918,
|
|
209
214
|
* link-local, or unique-local upstreams. Metadata endpoints remain blocked.
|
|
@@ -341,6 +346,12 @@ export interface OcxProviderConfig {
|
|
|
341
346
|
* Use for non-forward Responses gateways that reserve a hosted tool namespace server-side.
|
|
342
347
|
*/
|
|
343
348
|
modelPreferHostedTools?: Record<string, string[]>;
|
|
349
|
+
/**
|
|
350
|
+
* Whether the Responses upstream accepts OpenAI's extended hosted web_search fields.
|
|
351
|
+
* Set false only for a provider whose native contract rejects them; absence preserves
|
|
352
|
+
* passthrough compatibility for OpenAI and unclassified gateways.
|
|
353
|
+
*/
|
|
354
|
+
supportsOpenAiWebSearchToolFields?: boolean;
|
|
344
355
|
/**
|
|
345
356
|
* Provider-local repair for Responses gateways whose lifecycle snapshots omit canonical
|
|
346
357
|
* fields or closing events (#893). Disabled by default and applied only to client-facing
|
|
@@ -400,6 +411,13 @@ export interface OcxProviderConfig {
|
|
|
400
411
|
* mid-work; non-`openai-chat` adapters ignore this flag.
|
|
401
412
|
*/
|
|
402
413
|
terminalContinuationGuard?: boolean;
|
|
414
|
+
/**
|
|
415
|
+
* Opt-in for OpenAI-compatible chat gateways that may close after emitting a complete
|
|
416
|
+
* tool-call delta without `finish_reason` or `[DONE]`. The adapter accepts that EOF only
|
|
417
|
+
* when every pending call has a non-empty name and complete JSON-object arguments;
|
|
418
|
+
* incomplete JSON, missing arguments, and empty streams remain truncation errors.
|
|
419
|
+
*/
|
|
420
|
+
openaiChatEofTolerance?: boolean;
|
|
403
421
|
/**
|
|
404
422
|
* Opt-in: forward `prompt_cache_key` to the upstream `/chat/completions` body.
|
|
405
423
|
* OpenAI-specific extension; strict backends (Groq, Cerebras, etc.) reject unknown
|
package/src/types/request.ts
CHANGED
|
@@ -65,6 +65,11 @@ export interface OcxParsedRequest {
|
|
|
65
65
|
_clientThreadId?: string;
|
|
66
66
|
/** Provider/account/model-bound namespace for process-local raw-reasoning replay. */
|
|
67
67
|
_reasoningReplayScope?: OcxReasoningReplayScopeRef;
|
|
68
|
+
/**
|
|
69
|
+
* Set by bindRouteReasoningReplayScope after a proven serving-identity change, or by
|
|
70
|
+
* prepareOpaqueBlobRecovery after an authoritative rejection; consumers strip replayed blobs.
|
|
71
|
+
*/
|
|
72
|
+
_stripReasoningEncryptedContent?: boolean;
|
|
68
73
|
/**
|
|
69
74
|
* Optional authenticated tenant/operator namespace for Cursor thread→conversation derivation.
|
|
70
75
|
* When absent (single-operator local proxy), derivation stays local-scoped.
|
|
@@ -79,6 +84,10 @@ export interface OcxParsedRequest {
|
|
|
79
84
|
_kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
|
|
80
85
|
/** Provider-private continuation metadata resolved from the Responses previous_response_id chain. */
|
|
81
86
|
_providerContinuation?: OcxProviderContinuationState;
|
|
87
|
+
/** Persisted continuation considered only after the final physical route is known. */
|
|
88
|
+
_providerContinuationCandidate?: OcxProviderContinuationState;
|
|
89
|
+
/** Exact process-local route owner attached to newly persisted provider state. */
|
|
90
|
+
_providerContinuationOwner?: OcxProviderContinuationOwner;
|
|
82
91
|
/**
|
|
83
92
|
* The hosted `{type:"web_search", ...}` tool config, stashed when Codex enables web search. Routed
|
|
84
93
|
* (non-OpenAI) providers can't run it server-side, so the proxy re-exposes it as a function tool and
|
|
@@ -251,16 +260,33 @@ export interface OcxRequestOptions {
|
|
|
251
260
|
|
|
252
261
|
export type OcxMessagePhase = "commentary" | "final_answer";
|
|
253
262
|
|
|
263
|
+
/** Non-secret, process-local owner fence for provider-private continuation state. */
|
|
264
|
+
export interface OcxProviderContinuationOwner {
|
|
265
|
+
[field: string]: string | number;
|
|
266
|
+
version: 1;
|
|
267
|
+
providerName: string;
|
|
268
|
+
providerDestinationIdentity: string;
|
|
269
|
+
adapterName: string;
|
|
270
|
+
modelId: string;
|
|
271
|
+
credentialIdentity: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
254
274
|
/**
|
|
255
275
|
* Provider-private state that must follow a locally expanded `previous_response_id` chain.
|
|
256
276
|
* Kept out of public Responses output and persisted only in the bounded local continuation cache.
|
|
257
277
|
*/
|
|
258
278
|
export interface OcxProviderContinuationState {
|
|
279
|
+
/** Proxy-authored owner metadata; stripped before provider adapters receive the state. */
|
|
280
|
+
__ocxOwner?: OcxProviderContinuationOwner;
|
|
259
281
|
cursor?: {
|
|
282
|
+
[field: string]: unknown;
|
|
260
283
|
conversationId?: string;
|
|
261
284
|
checkpointUsable?: boolean;
|
|
285
|
+
/** Opaque process-local Cursor ConversationStateStructure snapshot ref. Never raw protobuf. */
|
|
286
|
+
checkpointRef?: string;
|
|
262
287
|
};
|
|
263
288
|
kiro?: {
|
|
289
|
+
[field: string]: unknown;
|
|
264
290
|
conversationId?: string;
|
|
265
291
|
};
|
|
266
292
|
[provider: string]: Record<string, unknown> | undefined;
|
package/src/types.ts
CHANGED
package/src/usage/log.ts
CHANGED
|
@@ -29,6 +29,7 @@ export type AttemptRecoveryKind =
|
|
|
29
29
|
| "rate-limit-429"
|
|
30
30
|
| "anthropic-oauth-429"
|
|
31
31
|
| "image-413"
|
|
32
|
+
| "opaque-blob-rejection"
|
|
32
33
|
| "empty-completion";
|
|
33
34
|
|
|
34
35
|
export interface PersistedUsageAttempt {
|
|
@@ -218,6 +219,7 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
|
|
|
218
219
|
"rate-limit-429",
|
|
219
220
|
"anthropic-oauth-429",
|
|
220
221
|
"image-413",
|
|
222
|
+
"opaque-blob-rejection",
|
|
221
223
|
"empty-completion",
|
|
222
224
|
]);
|
|
223
225
|
const USAGE_STATUSES = new Set<UsageStatus>([
|