@bitkyc08/opencodex 2.84.0 → 2.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -2,6 +2,7 @@ import type { OcxUsage } from "../../types";
|
|
|
2
2
|
import type { AgentServerMessage, McpArgs, ToolCall } from "./gen/agent_pb";
|
|
3
3
|
import { decodeCursorArgsMap } from "./arg-codec";
|
|
4
4
|
import { normalizeArgKeys } from "./arg-normalize";
|
|
5
|
+
import { canonicalCursorFreeformArgs } from "./freeform-args";
|
|
5
6
|
import {
|
|
6
7
|
CODEX_APPLY_PATCH_TOOL,
|
|
7
8
|
CURSOR_MULTI_EDIT_TOOL,
|
|
@@ -334,9 +335,20 @@ function toolSchemaForWireName(state: CursorProtobufEventState, toolName: string
|
|
|
334
335
|
return resolveShellBridgeAliasKey(toolName, alias => state.toolSchemas!.get(alias));
|
|
335
336
|
}
|
|
336
337
|
|
|
337
|
-
function
|
|
338
|
-
|
|
338
|
+
function isFreeformCall(state: CursorProtobufEventState, responsesName: string | undefined): boolean {
|
|
339
|
+
return responsesName !== undefined && state.freeformToolNames?.has(responsesName) === true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function decodeMcpArgsNormalized(
|
|
343
|
+
args: McpArgs | undefined,
|
|
344
|
+
state: CursorProtobufEventState,
|
|
345
|
+
openResponsesName?: string,
|
|
346
|
+
): string {
|
|
339
347
|
const toolName = mcpWireNameFromArgs(args);
|
|
348
|
+
const responsesName = toolName
|
|
349
|
+
? responsesToolNameFromCursorWire(toolName, state.cursorToolNameMap)
|
|
350
|
+
: openResponsesName;
|
|
351
|
+
const decoded = decodeCursorArgsMap(args?.args, { preserveStrings: isFreeformCall(state, responsesName) });
|
|
340
352
|
const schema = toolSchemaForWireName(state, toolName);
|
|
341
353
|
if (schema) return JSON.stringify(normalizeArgKeys(decoded, schema));
|
|
342
354
|
return JSON.stringify(decoded);
|
|
@@ -381,8 +393,13 @@ function normalizeJsonText(text: string, toolName: string | undefined, state: Cu
|
|
|
381
393
|
* preserved verbatim so the bridge can reject malformed or truncated JSON instead of silently
|
|
382
394
|
* converting it to `{}`. A genuinely empty buffer remains the no-argument case.
|
|
383
395
|
*/
|
|
384
|
-
function resolveCompletedArgs(
|
|
385
|
-
|
|
396
|
+
function resolveCompletedArgs(
|
|
397
|
+
buffered: string,
|
|
398
|
+
args: McpArgs | undefined,
|
|
399
|
+
state: CursorProtobufEventState,
|
|
400
|
+
openResponsesName?: string,
|
|
401
|
+
): string {
|
|
402
|
+
if (hasMcpArgBytes(args)) return decodeMcpArgsNormalized(args, state, openResponsesName);
|
|
386
403
|
const name = mcpWireNameFromArgs(args);
|
|
387
404
|
if (isCompleteJson(buffered)) return normalizeJsonText(buffered, name, state);
|
|
388
405
|
return buffered;
|
|
@@ -1092,7 +1109,7 @@ export function mapSyntheticMcpExecToToolEvents(
|
|
|
1092
1109
|
const out: CursorServerMessage[] = [...recordToolCall(options.state, callId, cursorWireName)];
|
|
1093
1110
|
if (out.some(event => event.type === "error")) return out;
|
|
1094
1111
|
const open = options.state.openToolCalls.get(callId);
|
|
1095
|
-
const finalArgs = resolveCompletedArgs(open?.args ?? "", args, options.state);
|
|
1112
|
+
const finalArgs = resolveCompletedArgs(open?.args ?? "", args, options.state, open?.name);
|
|
1096
1113
|
out.push(...commitToolCall(options.state, callId, finalArgs));
|
|
1097
1114
|
return out;
|
|
1098
1115
|
}
|
|
@@ -1167,23 +1184,16 @@ function discardBufferedTextToolCalls(state: CursorProtobufEventState): void {
|
|
|
1167
1184
|
* recorded in `openToolCalls`. Because each completion emits a whole non-interleaved unit, the bridge
|
|
1168
1185
|
* (which tracks a single current tool call) serializes parallel Cursor calls correctly.
|
|
1169
1186
|
*/
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
return !!parsed
|
|
1174
|
-
&& typeof parsed === "object"
|
|
1175
|
-
&& !Array.isArray(parsed)
|
|
1176
|
-
&& typeof (parsed as Record<string, unknown>).input === "string";
|
|
1177
|
-
} catch {
|
|
1178
|
-
return false;
|
|
1179
|
-
}
|
|
1187
|
+
/** True when the args already carry a body this call can commit, canonical or repairable (#7049). */
|
|
1188
|
+
function cursorFreeformArgsCommittable(args: string, toolName: string): boolean {
|
|
1189
|
+
return canonicalCursorFreeformArgs(args, toolName).ok;
|
|
1180
1190
|
}
|
|
1181
1191
|
|
|
1182
|
-
function dropInvalidFreeformCall(state: CursorProtobufEventState, callId: string, toolName: string): CursorServerMessage[] {
|
|
1192
|
+
function dropInvalidFreeformCall(state: CursorProtobufEventState, callId: string, toolName: string, reason: string): CursorServerMessage[] {
|
|
1183
1193
|
state.openToolCalls.delete(callId);
|
|
1184
1194
|
state.translatorBudget?.closeCall(callId);
|
|
1185
1195
|
state.completedToolCalls.add(callId);
|
|
1186
|
-
return [{ type: "error", message: `${toolName} call had invalid freeform arguments; expected {input:string}` }];
|
|
1196
|
+
return [{ type: "error", message: `${toolName} call had invalid freeform arguments; expected {input:string} (${reason})` }];
|
|
1187
1197
|
}
|
|
1188
1198
|
|
|
1189
1199
|
function dropShellBridgeCall(state: CursorProtobufEventState, callId: string, toolName: string): CursorServerMessage[] {
|
|
@@ -1202,11 +1212,14 @@ function dropStructuredEditCall(state: CursorProtobufEventState, callId: string,
|
|
|
1202
1212
|
return [{ type: "text", text: `\n${toolName} call was not converted to apply_patch: ${reason}` }];
|
|
1203
1213
|
}
|
|
1204
1214
|
|
|
1205
|
-
function commitToolCall(state: CursorProtobufEventState, callId: string,
|
|
1215
|
+
function commitToolCall(state: CursorProtobufEventState, callId: string, committedArgs: string): CursorServerMessage[] {
|
|
1206
1216
|
const open = state.openToolCalls.get(callId);
|
|
1207
1217
|
if (!open) return [];
|
|
1208
|
-
|
|
1209
|
-
|
|
1218
|
+
let finalArgs = committedArgs;
|
|
1219
|
+
if (isFreeformCall(state, open.name)) {
|
|
1220
|
+
const freeform = canonicalCursorFreeformArgs(finalArgs, open.name);
|
|
1221
|
+
if (!freeform.ok) return dropInvalidFreeformCall(state, callId, open.name, freeform.reason);
|
|
1222
|
+
finalArgs = freeform.args;
|
|
1210
1223
|
}
|
|
1211
1224
|
const schema = toolSchemaForWireName(state, open.name);
|
|
1212
1225
|
if (!cursorShellBridgeArgsValid(finalArgs, open.name, schema)) {
|
|
@@ -1386,7 +1399,7 @@ export function mapCursorProtobufServerMessage(
|
|
|
1386
1399
|
&& (
|
|
1387
1400
|
(name !== undefined && openBeforeStart.args.length === 0)
|
|
1388
1401
|
|| (state.freeformToolNames?.has(openBeforeStart.name) === true
|
|
1389
|
-
&& !
|
|
1402
|
+
&& !cursorFreeformArgsCommittable(openBeforeStart.args, openBeforeStart.name)
|
|
1390
1403
|
)
|
|
1391
1404
|
)
|
|
1392
1405
|
) {
|
|
@@ -1410,13 +1423,13 @@ export function mapCursorProtobufServerMessage(
|
|
|
1410
1423
|
if (
|
|
1411
1424
|
!openBeforeStart && open && !hasMcpArgBytes(args)
|
|
1412
1425
|
&& state.freeformToolNames?.has(open.name) === true
|
|
1413
|
-
&& !
|
|
1426
|
+
&& !cursorFreeformArgsCommittable(open.args, open.name)
|
|
1414
1427
|
) {
|
|
1415
1428
|
open.awaitingNativeArgs = true;
|
|
1416
1429
|
return [];
|
|
1417
1430
|
}
|
|
1418
1431
|
if (open) {
|
|
1419
|
-
const finalArgs = resolveCompletedArgs(open.args, args, state);
|
|
1432
|
+
const finalArgs = resolveCompletedArgs(open.args, args, state, open.name);
|
|
1420
1433
|
out.push(...commitToolCall(state, update.value.callId, finalArgs));
|
|
1421
1434
|
}
|
|
1422
1435
|
return out;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { OcxContentPart, OcxParsedRequest } from "../types";
|
|
3
|
-
import { antigravityUserAgent } from "./client-fingerprint";
|
|
3
|
+
import { antigravityCliUserAgent, antigravityUserAgent } from "./client-fingerprint";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Antigravity request User-Agent. Mirrors the real Antigravity IDE UA
|
|
@@ -11,6 +11,27 @@ import { antigravityUserAgent } from "./client-fingerprint";
|
|
|
11
11
|
*/
|
|
12
12
|
export const ANTIGRAVITY_REQUEST_UA = antigravityUserAgent();
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Antigravity CLI User-Agent. CCA serves Gemini 4 Argon only to the CLI client family, and it
|
|
16
|
+
* 404s other agent models (`gemini-3.7-flash-tiered`, `gemini-3.1-pro-low`) under it, so the
|
|
17
|
+
* choice is per wire model: see `antigravityUserAgentForWireModel`.
|
|
18
|
+
*/
|
|
19
|
+
export const ANTIGRAVITY_CLI_REQUEST_UA = antigravityCliUserAgent();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Model discovery (`:fetchAvailableModels`) uses the CLI UA so Argon's wire ids are listed;
|
|
23
|
+
* under the IDE UA they are absent.
|
|
24
|
+
*/
|
|
25
|
+
export const ANTIGRAVITY_DISCOVERY_UA = ANTIGRAVITY_CLI_REQUEST_UA;
|
|
26
|
+
|
|
27
|
+
/** Wire ids CCA serves only to the CLI client family (verified 2026-10-14). */
|
|
28
|
+
const ANTIGRAVITY_CLI_ONLY_WIRE_MODEL = /^gemini-4-argon(?:-(?:low|medium|high))?$/;
|
|
29
|
+
|
|
30
|
+
/** The User-Agent an Antigravity inference request for `wireModelId` must carry. */
|
|
31
|
+
export function antigravityUserAgentForWireModel(wireModelId: string): string {
|
|
32
|
+
return ANTIGRAVITY_CLI_ONLY_WIRE_MODEL.test(wireModelId) ? ANTIGRAVITY_CLI_REQUEST_UA : ANTIGRAVITY_REQUEST_UA;
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
/**
|
|
15
36
|
* Whether a stored `OcxToolCall.thoughtSignature` is a REAL upstream Gemini signature versus a
|
|
16
37
|
* foreign id that must not be forwarded to Gemini/Antigravity.
|
package/src/adapters/google.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { getVertexAccessToken } from "../lib/gcp-adc";
|
|
|
21
21
|
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
|
|
22
22
|
import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
|
|
23
23
|
import { isVertexTruncatedTurn, vertexTruncationErrorEvent } from "./google-truncation";
|
|
24
|
-
import {
|
|
24
|
+
import { antigravitySessionAnchor, antigravityUserAgentForWireModel, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
25
25
|
import { summarizeGoogleWireShape } from "./google-wire-shape";
|
|
26
26
|
import { compileGoogleWireBody } from "./google-wire-compiler";
|
|
27
27
|
import type { GoogleToolSchemaLossReport, GoogleToolSchemaProfile } from "./google-tool-schema";
|
|
@@ -1155,7 +1155,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1155
1155
|
requestId: `agent-${crypto.randomUUID()}`,
|
|
1156
1156
|
request,
|
|
1157
1157
|
};
|
|
1158
|
-
headers["User-Agent"] =
|
|
1158
|
+
headers["User-Agent"] = antigravityUserAgentForWireModel(wireModelId);
|
|
1159
1159
|
headers["Authorization"] = `Bearer ${token}`;
|
|
1160
1160
|
return { url, method: "POST", headers, body: JSON.stringify(envelope) };
|
|
1161
1161
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { diagnoseInvalidToolCalls, isRecord, type InvalidToolCallDiagnostic } from "./tool-call-validation";
|
|
2
2
|
import { TranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
|
|
3
|
-
import type { AdapterEvent, OcxUsage } from "../../types";
|
|
3
|
+
import type { AdapterEvent, OcxProviderConfig, OcxUsage } from "../../types";
|
|
4
|
+
import { createInlineThinkContentSplitter } from "../inline-think-tags";
|
|
4
5
|
|
|
5
6
|
export function stopReasonFor(finishReason: unknown): "max_tokens" | "content_filter" | undefined {
|
|
6
7
|
return finishReason === "length"
|
|
@@ -18,6 +19,63 @@ export function reasoningTextFrom(record: Record<string, unknown>): string | und
|
|
|
18
19
|
: undefined;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
type ChatContentEvent = Extract<AdapterEvent, { type: "text_delta" | "reasoning_raw_delta" | "error" }>;
|
|
23
|
+
|
|
24
|
+
/** Mistral can mix typed blocks and strings in one response. Other destinations keep their parser. */
|
|
25
|
+
export function chatContentEvents(content: unknown, baseUrl: string, usage?: OcxUsage, buffered = false): ChatContentEvent[] {
|
|
26
|
+
if (typeof content === "string") return content.length > 0 || buffered ? [{ type: "text_delta", text: content }] : [];
|
|
27
|
+
if (!Array.isArray(content)) return [];
|
|
28
|
+
let host: string;
|
|
29
|
+
try { host = new URL(baseUrl).hostname; } catch { return []; }
|
|
30
|
+
if (host !== "api.mistral.ai") return [];
|
|
31
|
+
const invalid = (): ChatContentEvent[] => [{
|
|
32
|
+
type: "error", status: 502, errorType: "upstream_error",
|
|
33
|
+
message: "unsupported Mistral content block", ...(usage ? { usage } : {}),
|
|
34
|
+
}];
|
|
35
|
+
const events: ChatContentEvent[] = [];
|
|
36
|
+
for (const block of content) {
|
|
37
|
+
if (!isRecord(block)) return invalid();
|
|
38
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
39
|
+
if (block.text.length > 0 || buffered) events.push({ type: "text_delta", text: block.text });
|
|
40
|
+
} else if (block.type === "thinking" && Array.isArray(block.thinking)) {
|
|
41
|
+
for (const inner of block.thinking) {
|
|
42
|
+
if (!isRecord(inner) || inner.type !== "text" || typeof inner.text !== "string") return invalid();
|
|
43
|
+
if (inner.text.length > 0) events.push({ type: "reasoning_raw_delta", text: inner.text });
|
|
44
|
+
}
|
|
45
|
+
} else return invalid();
|
|
46
|
+
}
|
|
47
|
+
return events;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Keep adjacent text together; typed reasoning flushes and resets inline-tag parsing. */
|
|
51
|
+
export function createChatContentSplitter(provider: OcxProviderConfig, modelId: string | undefined, budget: TranslatorBudget) {
|
|
52
|
+
let splitter = createInlineThinkContentSplitter(provider.inlineThinkTagModels, modelId, budget);
|
|
53
|
+
return {
|
|
54
|
+
feed(event: ChatContentEvent): AdapterEvent[] {
|
|
55
|
+
if (event.type === "error") return [event];
|
|
56
|
+
if (event.type === "text_delta") return splitter.feed(event.text);
|
|
57
|
+
const pending = splitter.flush();
|
|
58
|
+
splitter.dispose();
|
|
59
|
+
splitter = createInlineThinkContentSplitter(provider.inlineThinkTagModels, modelId, budget);
|
|
60
|
+
return [...pending, event];
|
|
61
|
+
},
|
|
62
|
+
flush: () => splitter.flush(),
|
|
63
|
+
dispose: () => splitter.dispose(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Split a buffered answer with the same typed-reasoning boundaries as a stream. */
|
|
68
|
+
export function splitChatContentEvents(content: unknown, provider: OcxProviderConfig, modelId: string | undefined, budget: TranslatorBudget, usage?: OcxUsage): AdapterEvent[] {
|
|
69
|
+
const events = chatContentEvents(content, provider.baseUrl, usage, true);
|
|
70
|
+
if (events.some(event => event.type === "error")) return events;
|
|
71
|
+
const splitter = createChatContentSplitter(provider, modelId, budget);
|
|
72
|
+
try {
|
|
73
|
+
return [...events.flatMap(event => splitter.feed(event)), ...splitter.flush()];
|
|
74
|
+
} finally {
|
|
75
|
+
splitter.dispose();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
21
79
|
export interface ReasoningDetailSegment {
|
|
22
80
|
key: string;
|
|
23
81
|
text: string;
|
|
@@ -5,7 +5,6 @@ import { applyExplicitChatReasoningWirePolicy } from "./openai-chat/reasoning-wi
|
|
|
5
5
|
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
|
|
6
6
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
|
|
7
7
|
import { modelInList } from "../types";
|
|
8
|
-
import { createInlineThinkContentSplitter, splitInlineThinkContent } from "./inline-think-tags";
|
|
9
8
|
import { mapReasoningEffort } from "../reasoning-effort";
|
|
10
9
|
import { debugProviderDiagnostic } from "../lib/debug";
|
|
11
10
|
import { sseFieldValue } from "../lib/sse-decoder";
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
type InvalidToolCallDiagnostic,
|
|
29
28
|
} from "./openai-chat/tool-call-validation";
|
|
30
29
|
import {
|
|
30
|
+
chatContentEvents, createChatContentSplitter, splitChatContentEvents,
|
|
31
31
|
createReasoningDetailSnapshotTracker,
|
|
32
32
|
invalidChoicesEvent,
|
|
33
33
|
invalidToolCallsEvent,
|
|
@@ -368,7 +368,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
368
368
|
const reasoningDetailsOptIn = modelInList(provider.reasoningDetailsModels, lastRequestedModelId ?? "");
|
|
369
369
|
// A gateway with no server-side reasoning parser leaves thinking inline in `content` as
|
|
370
370
|
// <think> blocks, which would otherwise render as the answer. Passthrough unless opted in.
|
|
371
|
-
const inlineThink =
|
|
371
|
+
const inlineThink = createChatContentSplitter(provider, lastRequestedModelId, budget);
|
|
372
372
|
const emitContent = function* (events: AdapterEvent[]): Generator<AdapterEvent> {
|
|
373
373
|
for (const event of events) {
|
|
374
374
|
// Any other event keeps its place behind held text instead of overtaking it.
|
|
@@ -444,8 +444,9 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
444
444
|
const reasoningText = reasoningTextFrom(delta);
|
|
445
445
|
if (reasoningText !== undefined) yield* toolCallContent.hold({ type: "reasoning_raw_delta", text: reasoningText });
|
|
446
446
|
}
|
|
447
|
-
|
|
448
|
-
yield*
|
|
447
|
+
for (const event of chatContentEvents(delta.content, provider.baseUrl, pendingUsage)) {
|
|
448
|
+
if (event.type === "error") return yield* terminateWithError(event);
|
|
449
|
+
yield* emitContent(inlineThink.feed(event));
|
|
449
450
|
}
|
|
450
451
|
|
|
451
452
|
const rawToolCalls = delta.tool_calls;
|
|
@@ -767,7 +768,9 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
767
768
|
}
|
|
768
769
|
if (reasoningText !== undefined) events.push({ type: "reasoning_raw_delta", text: reasoningText });
|
|
769
770
|
const contentStart = events.length;
|
|
770
|
-
|
|
771
|
+
const contentEvents = splitChatContentEvents(msg.content, provider, lastRequestedModelId, budget, usage);
|
|
772
|
+
if (contentEvents.some(event => event.type === "error")) return contentEvents;
|
|
773
|
+
events.push(...contentEvents);
|
|
771
774
|
const contentEnd = events.length;
|
|
772
775
|
const answerText = events.slice(contentStart).map(event => (event.type === "text_delta" ? event.text : "")).join("");
|
|
773
776
|
// Each call holds the delta event it emitted, so the batch repair sets its arguments later.
|
|
@@ -4,12 +4,50 @@ import type { AdapterRequest, ProviderAdapter } from "../base";
|
|
|
4
4
|
import { mapReasoningEffort } from "../../reasoning-effort";
|
|
5
5
|
import { buildSystemPrompt, MAX_TOOL_BLOCK_STARTS } from "../coding-agent/protocol";
|
|
6
6
|
import { buildCodingAgentToolCatalog, CodingAgentToolCatalogError } from "../coding-agent/tool-catalog";
|
|
7
|
-
import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type CodingAgentToolBridgeInput } from "../coding-agent/turn";
|
|
7
|
+
import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type CodingAgentToolBridgeInput, type CodingAgentFailure } from "../coding-agent/turn";
|
|
8
8
|
import { QODER_PROFILES, resolveQoderProfile, type QoderProfile } from "./profiles";
|
|
9
9
|
import { QoderScaffoldFilter, QODER_SCAFFOLD_ERROR_CODE, qoderScaffoldErrorMessage } from "./scaffold-guard";
|
|
10
10
|
|
|
11
11
|
export type QoderAdapterDeps = CodingAgentDeps;
|
|
12
12
|
|
|
13
|
+
function classifyQoderFailure(failure: CodingAgentFailure): Extract<AdapterEvent, { type: "error" }> {
|
|
14
|
+
const { error, profile } = failure;
|
|
15
|
+
const auth = (): typeof error => ({
|
|
16
|
+
...error, status: 401, errorType: "authentication_error", code: "invalid_api_key",
|
|
17
|
+
retryable: false, apiKeyRecovery: true,
|
|
18
|
+
});
|
|
19
|
+
if (failure.source === "process-exit") {
|
|
20
|
+
return failure.exitCode === 41 || (failure.exitCode === 1
|
|
21
|
+
&& failure.stderr.includes("QoderAuthError")
|
|
22
|
+
&& !!profile.tokenEnv && failure.stderr.includes(profile.tokenEnv)
|
|
23
|
+
&& /token (?:that )?was rejected/i.test(failure.stderr))
|
|
24
|
+
? auth() : error;
|
|
25
|
+
}
|
|
26
|
+
const code = failure.frame.error_code;
|
|
27
|
+
if (code === 105) return auth();
|
|
28
|
+
if (code === 114) return {
|
|
29
|
+
...error, status: 403, errorType: "permission_error", code: "permission_denied", retryable: false,
|
|
30
|
+
};
|
|
31
|
+
if (typeof code === "number" && [110, 113, 115, 116, 117, 118, 119, 122].includes(code)) {
|
|
32
|
+
return {
|
|
33
|
+
...error, status: 429, errorType: "insufficient_quota", code: "insufficient_quota", retryable: false,
|
|
34
|
+
...(code === 110 || code === 118 ? { apiKeyRecovery: true as const } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (typeof code === "number") return error;
|
|
38
|
+
if (/daily usage limit|credit usage limit/i.test(error.message)) {
|
|
39
|
+
const isAccountDaily = /^You've reached your daily usage limit for Chat\b/i.test(error.message.trim())
|
|
40
|
+
&& !/\b(?:team|model|shared)\b/i.test(error.message);
|
|
41
|
+
return {
|
|
42
|
+
...error, status: 429, errorType: "insufficient_quota", code: "insufficient_quota",
|
|
43
|
+
retryable: false,
|
|
44
|
+
...(isAccountDaily ? { apiKeyRecovery: true as const } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (/\binvalid (?:personal access )?token\b|\btoken (?:was |that was )?rejected\b|\bqoder authentication rejected\b/i.test(error.message)) return auth();
|
|
48
|
+
return error;
|
|
49
|
+
}
|
|
50
|
+
|
|
13
51
|
const QODER_MCP_SERVER_NAME = "opencodex";
|
|
14
52
|
const QODER_MCP_SERVER_PATH = fileURLToPath(new URL("../coding-agent/mcp-server.ts", import.meta.url));
|
|
15
53
|
|
|
@@ -164,6 +202,7 @@ export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapt
|
|
|
164
202
|
const system = buildSystemPrompt(parsed);
|
|
165
203
|
await runCodingAgentTurn({
|
|
166
204
|
profiles: QODER_PROFILES,
|
|
205
|
+
classifyFailure: classifyQoderFailure,
|
|
167
206
|
provider,
|
|
168
207
|
parsed,
|
|
169
208
|
incoming,
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execute ONE advisor consultation through the proxy's own /v1/chat/completions on loopback.
|
|
3
|
+
*
|
|
4
|
+
* Same execution shape as the vision sidecar's routed describe (src/vision/routed-describe.ts):
|
|
5
|
+
* the loopback call re-enters the normal data plane, so model resolution, provider auth, effort
|
|
6
|
+
* mapping and usage accounting are the ROUTING AUTHORITY's job — the advisor never builds its own
|
|
7
|
+
* router and never touches provider credentials. Any model string the router accepts works here:
|
|
8
|
+
* a bare native model, an explicit "provider/model", or an account-qualified native model.
|
|
9
|
+
*
|
|
10
|
+
* Recursion fence: the request carries `x-opencodex-advisor-internal` set to this process's
|
|
11
|
+
* capability. The Chat surface checks that value before its bridge rebuilds headers and carries
|
|
12
|
+
* the fact into handleResponses as `advisorInternal`. A marked request never plans an advisor
|
|
13
|
+
* consultation (depth cap 1 — the same structure as the vision describe fence). The capability
|
|
14
|
+
* is not a literal and is not forwarded upstream.
|
|
15
|
+
*
|
|
16
|
+
* Failure contract: never throws. A failed consultation returns `ok: false` plus a redacted,
|
|
17
|
+
* bounded error string; the worker keeps going (fail-open) either with an explicit
|
|
18
|
+
* unavailable context or with nothing, depending on the trigger.
|
|
19
|
+
*/
|
|
20
|
+
import type { OcxConfig } from "../types";
|
|
21
|
+
import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
|
|
22
|
+
import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
|
|
23
|
+
import { redactSecretString } from "../lib/redact";
|
|
24
|
+
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
25
|
+
import {
|
|
26
|
+
ADVISOR_INTERNAL_CAPABILITY_HEADER,
|
|
27
|
+
internalCallCapability,
|
|
28
|
+
} from "../lib/local-internal-call-capability";
|
|
29
|
+
import { configuredPort } from "../server/auth-cors";
|
|
30
|
+
import { ADVISOR_SYSTEM_INSTRUCTION, buildAdvisorUserPrompt, type AdvisorContextInput } from "./context";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The header the advisor presents on its own loopback request. Its VALUE is this process's
|
|
34
|
+
* internal-call capability — never a literal, and never trusted from an inbound caller.
|
|
35
|
+
*/
|
|
36
|
+
export { ADVISOR_INTERNAL_CAPABILITY_HEADER as ADVISOR_INTERNAL_HEADER } from "../lib/local-internal-call-capability";
|
|
37
|
+
|
|
38
|
+
/** Bound the loopback JSON response; advice is prose, not data dumps. */
|
|
39
|
+
const MAX_ADVISOR_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
40
|
+
/** Advice length cap handed to the worker. */
|
|
41
|
+
const MAX_ADVICE_CHARS = 16_000;
|
|
42
|
+
|
|
43
|
+
export interface AdvisorConsultationResult {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
/** Advisor prose when ok; empty string otherwise. */
|
|
46
|
+
advice: string;
|
|
47
|
+
advisorModel: string;
|
|
48
|
+
/** True when the caller aborted the request — not a provider failure (no cooldown). */
|
|
49
|
+
cancelled?: boolean;
|
|
50
|
+
/** Redacted, bounded failure description when not ok. */
|
|
51
|
+
error?: string;
|
|
52
|
+
/** Loopback round-trip duration (ms), for logs. */
|
|
53
|
+
durationMs: number;
|
|
54
|
+
/** Token usage reported by the chat completion, when the adapter surfaced it. */
|
|
55
|
+
usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function advisorDestinationOrigin(
|
|
59
|
+
config: Pick<OcxConfig, "port" | "hostname" | "unauthenticatedLoopbackListener">,
|
|
60
|
+
): string {
|
|
61
|
+
// Same port resolution rule as the vision sidecar: config.port can be 0 (ephemeral bind, tests)
|
|
62
|
+
// or stale after a live port override, so prefer the recorded actual bind port when present.
|
|
63
|
+
const port = config.port && config.port > 0
|
|
64
|
+
? config.port
|
|
65
|
+
: Number(configuredPort()) || 10_100;
|
|
66
|
+
return localInferenceDestination(config, port).origin;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extractContent(payload: unknown): string | undefined {
|
|
70
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
71
|
+
const choices = (payload as { choices?: unknown }).choices;
|
|
72
|
+
if (!Array.isArray(choices) || choices.length === 0) return undefined;
|
|
73
|
+
const message = (choices[0] as { message?: unknown })?.message;
|
|
74
|
+
if (!message || typeof message !== "object") return undefined;
|
|
75
|
+
const content = (message as { content?: unknown }).content;
|
|
76
|
+
if (typeof content === "string" && content.trim().length > 0) return content;
|
|
77
|
+
if (Array.isArray(content)) {
|
|
78
|
+
const joined = content
|
|
79
|
+
.map(part => (part && typeof part === "object" && typeof (part as { text?: unknown }).text === "string"
|
|
80
|
+
? (part as { text: string }).text
|
|
81
|
+
: ""))
|
|
82
|
+
.join("");
|
|
83
|
+
if (joined.trim().length > 0) return joined;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractUsage(payload: unknown): AdvisorConsultationResult["usage"] {
|
|
89
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
90
|
+
const usage = (payload as { usage?: unknown }).usage;
|
|
91
|
+
if (!usage || typeof usage !== "object") return undefined;
|
|
92
|
+
const num = (value: unknown): number | undefined => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
93
|
+
const inputTokens = num((usage as { prompt_tokens?: unknown }).prompt_tokens);
|
|
94
|
+
const outputTokens = num((usage as { completion_tokens?: unknown }).completion_tokens);
|
|
95
|
+
const totalTokens = num((usage as { total_tokens?: unknown }).total_tokens);
|
|
96
|
+
if (inputTokens === undefined && outputTokens === undefined && totalTokens === undefined) return undefined;
|
|
97
|
+
return { ...(inputTokens !== undefined ? { inputTokens } : {}), ...(outputTokens !== undefined ? { outputTokens } : {}), ...(totalTokens !== undefined ? { totalTokens } : {}) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Base URL seam for tests; production always self-fetches the resolved local destination.
|
|
102
|
+
* The advisor is an internal caller: no credential material rides the override path in tests.
|
|
103
|
+
*/
|
|
104
|
+
export function advisorBaseUrl(
|
|
105
|
+
config: Pick<OcxConfig, "port" | "hostname" | "unauthenticatedLoopbackListener">,
|
|
106
|
+
): string {
|
|
107
|
+
return advisorDestinationOrigin(config);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function consultAdvisor(
|
|
111
|
+
input: AdvisorContextInput,
|
|
112
|
+
config: Pick<OcxConfig, "port" | "hostname" | "apiKeys" | "unauthenticatedLoopbackListener">,
|
|
113
|
+
effort: string,
|
|
114
|
+
timeoutMs: number,
|
|
115
|
+
abortSignal?: AbortSignal,
|
|
116
|
+
baseUrlOverride?: string,
|
|
117
|
+
): Promise<AdvisorConsultationResult> {
|
|
118
|
+
const t0 = Date.now();
|
|
119
|
+
const headers: Record<string, string> = {
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
[ADVISOR_INTERNAL_CAPABILITY_HEADER]: internalCallCapability(),
|
|
122
|
+
};
|
|
123
|
+
// Admission ladder identical to the vision sidecar: env token || service token file || first
|
|
124
|
+
// configured API key, sent as `x-opencodex-api-key` — never Authorization. Loopback binds that
|
|
125
|
+
// require no token simply omit it.
|
|
126
|
+
const admission = localAdmissionToken(config);
|
|
127
|
+
if (admission) headers["x-opencodex-api-key"] = admission;
|
|
128
|
+
|
|
129
|
+
const requestBody = {
|
|
130
|
+
model: input.advisorModel,
|
|
131
|
+
stream: false,
|
|
132
|
+
reasoning_effort: effort,
|
|
133
|
+
messages: [
|
|
134
|
+
{ role: "system", content: ADVISOR_SYSTEM_INSTRUCTION },
|
|
135
|
+
{ role: "user", content: buildAdvisorUserPrompt(input) },
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const linkedSignal = signalWithTimeout(timeoutMs, abortSignal);
|
|
140
|
+
const sidecarExit = sidecarEnter("advisor");
|
|
141
|
+
if (abortSignal?.aborted) {
|
|
142
|
+
sidecarExit();
|
|
143
|
+
linkedSignal.cleanup();
|
|
144
|
+
return { ok: false, cancelled: true, advice: "", advisorModel: input.advisorModel, error: "advisor cancelled before dispatch", durationMs: Date.now() - t0 };
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const res = await fetch(`${baseUrlOverride ?? advisorBaseUrl(config)}/v1/chat/completions`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers,
|
|
150
|
+
body: JSON.stringify(requestBody),
|
|
151
|
+
signal: linkedSignal.signal,
|
|
152
|
+
redirect: "manual",
|
|
153
|
+
});
|
|
154
|
+
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
155
|
+
try {
|
|
156
|
+
// Bounded read: count bytes per chunk and cancel the reader the moment the bound is
|
|
157
|
+
// exceeded, instead of materializing the full body before the length check.
|
|
158
|
+
let raw = "";
|
|
159
|
+
let totalBytes = 0;
|
|
160
|
+
const reader = res.body?.getReader();
|
|
161
|
+
if (reader) {
|
|
162
|
+
const decoder = new TextDecoder();
|
|
163
|
+
for (;;) {
|
|
164
|
+
const { done, value } = await reader.read();
|
|
165
|
+
if (done) break;
|
|
166
|
+
totalBytes += value.byteLength;
|
|
167
|
+
if (totalBytes > MAX_ADVISOR_RESPONSE_BYTES) {
|
|
168
|
+
try { await reader.cancel(); } catch { /* body already closing */ }
|
|
169
|
+
return { ok: false, advice: "", advisorModel: input.advisorModel, error: "advisor response exceeded byte bound", durationMs: Date.now() - t0 };
|
|
170
|
+
}
|
|
171
|
+
raw += decoder.decode(value, { stream: true });
|
|
172
|
+
}
|
|
173
|
+
raw += decoder.decode();
|
|
174
|
+
}
|
|
175
|
+
const durationMs = Date.now() - t0;
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
// Status only. Upstream bodies can echo the consultation prompt; they are not logged
|
|
178
|
+
// and not handed to the worker. display-safe body text is intentionally not copied here.
|
|
179
|
+
return {
|
|
180
|
+
ok: false, advice: "", advisorModel: input.advisorModel,
|
|
181
|
+
error: `advisor HTTP ${res.status}`,
|
|
182
|
+
durationMs,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
let payload: unknown;
|
|
186
|
+
try { payload = JSON.parse(raw); } catch {
|
|
187
|
+
return { ok: false, advice: "", advisorModel: input.advisorModel, error: "advisor returned non-JSON", durationMs };
|
|
188
|
+
}
|
|
189
|
+
const content = extractContent(payload);
|
|
190
|
+
if (!content) {
|
|
191
|
+
return { ok: false, advice: "", advisorModel: input.advisorModel, error: "advisor returned no text", durationMs };
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
ok: true,
|
|
195
|
+
advice: content.length > MAX_ADVICE_CHARS ? `${content.slice(0, MAX_ADVICE_CHARS)}… [truncated]` : content,
|
|
196
|
+
advisorModel: input.advisorModel,
|
|
197
|
+
durationMs,
|
|
198
|
+
...(extractUsage(payload) ? { usage: extractUsage(payload) } : {}),
|
|
199
|
+
};
|
|
200
|
+
} finally {
|
|
201
|
+
detachBodyGuard();
|
|
202
|
+
}
|
|
203
|
+
} catch (e) {
|
|
204
|
+
const durationMs = Date.now() - t0;
|
|
205
|
+
// Client cancellation is not a provider failure: the caller aborted (linked signal), so the
|
|
206
|
+
// task must stay eligible for a later attempt rather than entering a failure cooldown.
|
|
207
|
+
if (abortSignal?.aborted) {
|
|
208
|
+
return { ok: false, cancelled: true, advice: "", advisorModel: input.advisorModel, error: "advisor cancelled by the caller", durationMs };
|
|
209
|
+
}
|
|
210
|
+
const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error";
|
|
211
|
+
return {
|
|
212
|
+
ok: false, advice: "", advisorModel: input.advisorModel,
|
|
213
|
+
error: `advisor ${kind}: ${redactSecretString(e instanceof Error ? e.message : String(e))}`,
|
|
214
|
+
durationMs,
|
|
215
|
+
};
|
|
216
|
+
} finally {
|
|
217
|
+
sidecarExit();
|
|
218
|
+
linkedSignal.cleanup();
|
|
219
|
+
}
|
|
220
|
+
}
|