@bitkyc08/opencodex 2.59.0 → 2.60.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-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
package/src/adapters/google.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
|
|
2
|
-
import { debugDroppedFrame } from "../lib/debug";
|
|
2
|
+
import { debugDroppedFrame, debugProviderDiagnosticLazy } from "../lib/debug";
|
|
3
3
|
import { createToolCallIdAllocator } from "./tool-call-id";
|
|
4
4
|
import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
|
|
5
5
|
import type {
|
|
@@ -20,8 +20,10 @@ import { getVertexAccessToken } from "../lib/gcp-adc";
|
|
|
20
20
|
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
|
|
21
21
|
import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
|
|
22
22
|
import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-truncation";
|
|
23
|
-
import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
23
|
+
import { ANTIGRAVITY_REQUEST_UA, antigravitySessionAnchor, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
24
|
+
import { summarizeGoogleWireShape } from "./google-wire-shape";
|
|
24
25
|
import { compileGoogleWireBody } from "./google-wire-compiler";
|
|
26
|
+
import type { GoogleToolSchemaLossReport, GoogleToolSchemaProfile } from "./google-tool-schema";
|
|
25
27
|
import { identifyRoutedModel } from "./identity";
|
|
26
28
|
import {
|
|
27
29
|
antigravityUsesReplayCache,
|
|
@@ -737,6 +739,14 @@ function invalidGoogleShapeEvent(
|
|
|
737
739
|
}
|
|
738
740
|
|
|
739
741
|
export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
742
|
+
const toolSchemaPolicy = provider.googleToolSchemaPolicy ?? "compatible";
|
|
743
|
+
const toolSchemaProfile = {
|
|
744
|
+
endpointClass: provider.googleMode ?? "ai-studio",
|
|
745
|
+
} satisfies GoogleToolSchemaProfile;
|
|
746
|
+
const reportToolSchemaLoss = (report: GoogleToolSchemaLossReport): void => {
|
|
747
|
+
if (!report.lossy && report.uncertainComparisons === 0) return;
|
|
748
|
+
debugProviderDiagnosticLazy("google", "google-tool-schema-loss", () => ({ ...report }));
|
|
749
|
+
};
|
|
740
750
|
// Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
|
|
741
751
|
// can stash the CCA model/session for parseStream's reasoning-replay observation.
|
|
742
752
|
let antigravityModel: string | undefined;
|
|
@@ -799,7 +809,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
799
809
|
...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
|
|
800
810
|
? {
|
|
801
811
|
fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
|
|
802
|
-
(provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(
|
|
812
|
+
(provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(
|
|
813
|
+
request,
|
|
814
|
+
ctx,
|
|
815
|
+
{ toolSchemaProfile, toolSchemaPolicy },
|
|
816
|
+
),
|
|
803
817
|
formatErrorBody: (status: number, _headers: Headers, payloadText: string): string =>
|
|
804
818
|
(provider.googleMode === "cloud-code-assist" ? safeAntigravityHttpErrorMessage : safeVertexHttpErrorMessage)(status, payloadText),
|
|
805
819
|
}
|
|
@@ -970,7 +984,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
970
984
|
const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
|
|
971
985
|
draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
|
|
972
986
|
}
|
|
973
|
-
const compiled = compileGoogleWireBody(draftRequest);
|
|
987
|
+
const compiled = compileGoogleWireBody(draftRequest, toolSchemaProfile, toolSchemaPolicy);
|
|
988
|
+
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
974
989
|
const request = compiled.body;
|
|
975
990
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
976
991
|
// Compile names before replay: signatures are keyed by the exact provider-visible name.
|
|
@@ -989,6 +1004,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
989
1004
|
// Vertex and AI Studio share one decision. A second check here would append a
|
|
990
1005
|
// duplicate nudge whenever signature sanitization reshapes the tail afterwards.
|
|
991
1006
|
}
|
|
1007
|
+
// Opt-in structural description of the request that is about to leave (#5008).
|
|
1008
|
+
//
|
|
1009
|
+
// Passed as a BUILDER, not a value: the lazy form gates before invoking it, so a session
|
|
1010
|
+
// with provider debug off never pays the walk, and it evaluates the projection inside its
|
|
1011
|
+
// own try/catch, so a throw in here cannot turn a built request into a rejected one. The
|
|
1012
|
+
// projection only reads `request`, so the bytes below are the same either way.
|
|
1013
|
+
debugProviderDiagnosticLazy("google", "antigravity-wire-shape", () => summarizeGoogleWireShape(request, {
|
|
1014
|
+
sessionAnchor: antigravitySessionAnchor(parsed),
|
|
1015
|
+
// Signed at translation time, from client history or the durable store. The
|
|
1016
|
+
// Antigravity session cache signs afterwards, inside applyAntigravityReplay, and the
|
|
1017
|
+
// projection attributes that remainder to the cache rather than to this count.
|
|
1018
|
+
historySignedCalls: replayedCallIds.length,
|
|
1019
|
+
replayScopeBound: parsed._reasoningReplayScope !== undefined,
|
|
1020
|
+
}));
|
|
992
1021
|
const envelope = {
|
|
993
1022
|
model: wireModelId,
|
|
994
1023
|
// The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from
|
|
@@ -1006,7 +1035,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1006
1035
|
}
|
|
1007
1036
|
|
|
1008
1037
|
if (provider.googleMode === "vertex") {
|
|
1009
|
-
const compiled = compileGoogleWireBody(body);
|
|
1038
|
+
const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
|
|
1039
|
+
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
1010
1040
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
1011
1041
|
const vertexProject = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || "api-key";
|
|
1012
1042
|
const vertexLocation = provider.location || process.env.GOOGLE_CLOUD_LOCATION || "global";
|
|
@@ -1052,7 +1082,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1052
1082
|
if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
|
|
1053
1083
|
headers["x-goog-api-key"] = apiKey;
|
|
1054
1084
|
|
|
1055
|
-
const compiled = compileGoogleWireBody(body);
|
|
1085
|
+
const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
|
|
1086
|
+
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
1056
1087
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
1057
1088
|
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
1058
1089
|
},
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
export const OPENAI_CHAT_IMAGE_BASE64_BUDGET = 3_670_016; // 3.5MiB
|
|
14
14
|
|
|
15
15
|
export interface NormalizeOpenAIChatImagesOptions
|
|
16
|
-
extends Pick<NormalizeOptions, "encode" | "tierBias" | "validate"> {
|
|
16
|
+
extends Pick<NormalizeOptions, "encode" | "tierBias" | "validate"> {
|
|
17
|
+
abortSignal?: AbortSignal;
|
|
18
|
+
}
|
|
17
19
|
|
|
18
20
|
/** Whether `value` is a plain object, so message and part shapes can be walked safely. */
|
|
19
21
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -297,7 +297,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
297
297
|
};
|
|
298
298
|
};
|
|
299
299
|
if (hasShrinkableOpenAIChatImages(messages)) {
|
|
300
|
-
|
|
300
|
+
const imageOptions = { tierBias: incoming?.imageTierBias, abortSignal: incoming?.abortSignal };
|
|
301
|
+
return normalizeOpenAIChatImages(messages, imageOptions).then(finish, error => {
|
|
302
|
+
if (incoming?.abortSignal?.aborted) throw error;
|
|
303
|
+
return finish();
|
|
304
|
+
});
|
|
301
305
|
}
|
|
302
306
|
return finish();
|
|
303
307
|
},
|
|
@@ -101,14 +101,17 @@ export function preferConfiguredHostedTools(
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
let input = body.input;
|
|
104
|
-
|
|
104
|
+
// Indices arrive in increasing order, so the first stripped container is already
|
|
105
|
+
// the minimum — tracking it directly avoids spreading an attacker-sized Set into
|
|
106
|
+
// Math.min's argument list.
|
|
107
|
+
let firstStrippedAdditionalToolsIndex: number | undefined;
|
|
105
108
|
if (Array.isArray(body.input)) {
|
|
106
109
|
let nestedChanged = false;
|
|
107
110
|
const mappedInput = body.input.map((item, index) => {
|
|
108
111
|
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
|
|
109
112
|
const nestedTools = stripGroup(item.tools);
|
|
110
113
|
if (nestedTools === item.tools) return item;
|
|
111
|
-
|
|
114
|
+
firstStrippedAdditionalToolsIndex ??= index;
|
|
112
115
|
nestedChanged = true;
|
|
113
116
|
return { ...item, tools: nestedTools };
|
|
114
117
|
});
|
|
@@ -127,17 +130,16 @@ export function preferConfiguredHostedTools(
|
|
|
127
130
|
|| (Array.isArray(input) && input.some(item => isPlainObject(item)
|
|
128
131
|
&& item.type === "additional_tools"
|
|
129
132
|
&& hasHostedImageGenTool(item.tools)));
|
|
130
|
-
if ((strippedTopLevelImageGenTool ||
|
|
133
|
+
if ((strippedTopLevelImageGenTool || firstStrippedAdditionalToolsIndex !== undefined) && !hasHostedImageGenDeclaration) {
|
|
131
134
|
if (strippedTopLevelImageGenTool && Array.isArray(tools)) {
|
|
132
135
|
tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }];
|
|
133
|
-
} else if (
|
|
136
|
+
} else if (firstStrippedAdditionalToolsIndex !== undefined && Array.isArray(input)) {
|
|
134
137
|
// Restore into the FIRST stripped container only. Tool declarations are
|
|
135
138
|
// request-scoped, not container-scoped — the containers are separate carriers for
|
|
136
139
|
// one tool set, so a single hosted declaration covers the request. An earlier
|
|
137
140
|
// revision restored into every stripped container and put `image_generation` on
|
|
138
141
|
// the wire twice; review caught it.
|
|
139
|
-
|
|
140
|
-
input = input.map((item, index) => index === firstStripped
|
|
142
|
+
input = input.map((item, index) => index === firstStrippedAdditionalToolsIndex
|
|
141
143
|
&& isPlainObject(item)
|
|
142
144
|
&& Array.isArray(item.tools)
|
|
143
145
|
? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] }
|
|
@@ -18,6 +18,7 @@ import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
|
18
18
|
import { rewriteRoutedCustomToolsForUpstream } from "../../responses/custom-tool-compat";
|
|
19
19
|
import { rewriteRoutedToolSearchForUpstream } from "../../responses/tool-search-compat";
|
|
20
20
|
import { rewriteRoutedNamespaceToolsForUpstream } from "../../responses/namespace-tool-compat";
|
|
21
|
+
import { repairLegacyDottedToolCallNames } from "../../responses/legacy-dotted-tool-name-repair";
|
|
21
22
|
import { preparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
|
|
22
23
|
import { isMetaAiResponsesDestination, rewriteMuseToolNamesForUpstream } from "../../responses/muse-tool-name-alias";
|
|
23
24
|
import { openaiResponsesUrl } from "../openai-responses-url";
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
import {
|
|
32
33
|
createAdapterTierMetadata,
|
|
33
34
|
} from "../../providers/fastwire";
|
|
34
|
-
import { mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
|
|
35
|
+
import { dropResponsesReasoningInputItems, mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
|
|
35
36
|
import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
|
|
36
37
|
import { stripCanonicalForwardPromptCacheOptions, stripDeprecatedPromptCacheRetention } from "./prompt-cache";
|
|
37
38
|
import { isPlainObject } from "./internal";
|
|
@@ -295,6 +296,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
295
296
|
if (forward || stateless || pairedToolResults) {
|
|
296
297
|
outBody = repairOrphanedInputItems(outBody, unexpandedMiss, synthesizeMissingCallOutputs);
|
|
297
298
|
}
|
|
299
|
+
if (provider.dropResponsesReasoningItems === true) {
|
|
300
|
+
outBody = dropResponsesReasoningInputItems(outBody);
|
|
301
|
+
}
|
|
298
302
|
if (adjacentToolResults) {
|
|
299
303
|
outBody = normalizeResponsesToolResultAdjacency(outBody);
|
|
300
304
|
}
|
|
@@ -335,6 +339,12 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
335
339
|
// every turn, and a strict parser rejects the whole request over the missing key —
|
|
336
340
|
// `queries` for DeepSeek (#930), `query` for Console Go (#3071).
|
|
337
341
|
outBody = backfillWebSearchQueries(outBody);
|
|
342
|
+
// #5095: a conversation that already contains a `default.`-prefixed call name is refused by
|
|
343
|
+
// the upstream `^[a-zA-Z0-9_-]+$` name pattern on every later turn that replays it, so the
|
|
344
|
+
// task cannot be compacted or continued at all. Repair the replayed item here, before the
|
|
345
|
+
// canonical-destination split below, because the reported failure was a side chat on a plain
|
|
346
|
+
// OpenAI model inheriting history a routed provider had damaged.
|
|
347
|
+
outBody = repairLegacyDottedToolCallNames(outBody);
|
|
338
348
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
339
349
|
outBody = stripInternalChatMessageMetadataPassthrough(outBody);
|
|
340
350
|
// The same class of private field, one level up, but keyed on the DESTINATION rather than
|
|
@@ -397,14 +407,16 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
397
407
|
// Last, so promoted namespace children are also cleared of Codex-private fields.
|
|
398
408
|
outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
|
|
399
409
|
}
|
|
400
|
-
if (!forward)
|
|
410
|
+
if (!forward) {
|
|
411
|
+
outBody = normalizeOpenCodeGoAdditionalTools(outBody, url, parsed._replayPrefixLen);
|
|
412
|
+
}
|
|
401
413
|
// Same predicate as the routedCompaction gate in handleResponses(): an authMode check would
|
|
402
414
|
// let a noncanonical custom forward provider skip this rewrite while the server still routes
|
|
403
415
|
// it as a summarizer turn (#422). The compaction body build removes the tool surface and must
|
|
404
416
|
// therefore be the last routed transform that may depend on those declarations. Structural
|
|
405
417
|
// sanitizers below can still run after it.
|
|
406
418
|
outBody = normalizeResponsesCodeMode(outBody, parsed, provider);
|
|
407
|
-
if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
|
|
419
|
+
if (parsed._compactionRequest === true && (!isCanonicalOpenAiForwardProvider(provider) || parsed._portableCompaction === true)) {
|
|
408
420
|
outBody = buildRoutedCompactionBody(outBody);
|
|
409
421
|
}
|
|
410
422
|
// Run after routed compaction so nested input_image parts are replaced before a malformed
|
|
@@ -4,6 +4,13 @@ import { OCX_REASONING_PREFIX } from "../../responses/reasoning-envelope";
|
|
|
4
4
|
import { configuredReasoningEfforts, mapReasoningEffort, modelRecordValue } from "../../reasoning-effort";
|
|
5
5
|
import { isPlainObject } from "./internal";
|
|
6
6
|
|
|
7
|
+
/** Drop only replayed Responses reasoning items; all other continuation input stays untouched. */
|
|
8
|
+
export function dropResponsesReasoningInputItems(body: unknown): unknown {
|
|
9
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
10
|
+
const input = body.input.filter(item => !isPlainObject(item) || item.type !== "reasoning");
|
|
11
|
+
return input.length === body.input.length ? body : { ...body, input };
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Sanitize reasoning input by field policy, not by preserving each item's shape. Retaining a
|
|
9
16
|
* native `encrypted_content` guarantees only that blob value: `status` is always removed;
|
|
@@ -3,7 +3,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
/** Console Go accepts public tools but rejects the private additional_tools input wrapper. */
|
|
6
|
-
export function normalizeOpenCodeGoAdditionalTools(
|
|
6
|
+
export function normalizeOpenCodeGoAdditionalTools(
|
|
7
|
+
body: unknown,
|
|
8
|
+
responseUrl: string,
|
|
9
|
+
replayPrefixLength = 0,
|
|
10
|
+
): unknown {
|
|
7
11
|
let destination: URL;
|
|
8
12
|
try {
|
|
9
13
|
destination = new URL(responseUrl);
|
|
@@ -20,10 +24,16 @@ export function normalizeOpenCodeGoAdditionalTools(body: unknown, responseUrl: s
|
|
|
20
24
|
|
|
21
25
|
const input: unknown[] = [];
|
|
22
26
|
const promoted: unknown[] = [];
|
|
27
|
+
const currentTurnStart = Number.isFinite(replayPrefixLength)
|
|
28
|
+
? Math.min(body.input.length, Math.max(0, Math.trunc(replayPrefixLength)))
|
|
29
|
+
: 0;
|
|
23
30
|
let changed = false;
|
|
24
|
-
for (const item of body.input) {
|
|
31
|
+
for (const [index, item] of body.input.entries()) {
|
|
25
32
|
if (isRecord(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
|
|
26
33
|
changed = true;
|
|
34
|
+
// Replayed wrappers are conversation history, not authority for the current request.
|
|
35
|
+
// Go cannot accept the wrapper itself, so remove it without promoting its catalog.
|
|
36
|
+
if (index < currentTurnStart) continue;
|
|
27
37
|
// Custom/search/namespace lowering already owns identity and deduplication. This pass
|
|
28
38
|
// only moves declarations, including hosted tools that intentionally have no name.
|
|
29
39
|
for (const tool of item.tools) promoted.push(tool);
|
package/src/bridge/sse.ts
CHANGED
|
@@ -17,11 +17,10 @@ import {
|
|
|
17
17
|
} from "../lib/errors";
|
|
18
18
|
import { redactSecretString } from "../lib/redact";
|
|
19
19
|
import {
|
|
20
|
-
freeformFallbackKeys,
|
|
21
20
|
mayBecomePatchEnvelope,
|
|
22
21
|
repairFreeformToolInput,
|
|
23
|
-
unwrapFreeformToolInput,
|
|
24
22
|
} from "../responses/apply-patch-envelope";
|
|
23
|
+
import { progressiveFreeformInput } from "../responses/progressive-freeform-input";
|
|
25
24
|
import { encodeCompactionSummary } from "../responses/compaction";
|
|
26
25
|
import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
|
|
27
26
|
import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
|
|
@@ -153,153 +152,6 @@ export function bridgeToResponsesSSE(
|
|
|
153
152
|
? compileCodeModeHelperInput(args, helper, codeModeHelperName ?? toolName)
|
|
154
153
|
: repairFreeformToolInput(args, toolName, namespace);
|
|
155
154
|
};
|
|
156
|
-
// Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
|
|
157
|
-
// (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
|
|
158
|
-
// the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
|
|
159
|
-
// buffers get their string value progressively unescaped; anything else streams raw.
|
|
160
|
-
const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
|
|
161
|
-
type WrapperOpening =
|
|
162
|
-
| { state: "none" }
|
|
163
|
-
| { state: "prefix" }
|
|
164
|
-
| { state: "open"; valueStart: number };
|
|
165
|
-
/**
|
|
166
|
-
* Where the string value of `{"<key>":"` begins, tolerating the insignificant whitespace
|
|
167
|
-
* `JSON.parse` accepts.
|
|
168
|
-
*
|
|
169
|
-
* The earlier form of this compared the buffer against the compact literal `{"key":"`, so a
|
|
170
|
-
* wrapper written with spaces or newlines matched no prefix at all, streamed as raw JSON
|
|
171
|
-
* deltas and then completed as the unwrapped body. That is the same delta/completion
|
|
172
|
-
* disagreement #5047 closed for compact wrappers, reached through a different spelling:
|
|
173
|
-
* `unwrapFreeformToolInput` reads the completed text with `JSON.parse`, which does not care
|
|
174
|
-
* how the object is laid out, so neither can the streaming side.
|
|
175
|
-
*/
|
|
176
|
-
const wrapperOpening = (args: string, key: string): WrapperOpening => {
|
|
177
|
-
let index = 0;
|
|
178
|
-
for (const token of ["{", `"${key}"`, ":", '"']) {
|
|
179
|
-
while (index < args.length && JSON_WHITESPACE.has(args[index]!)) index++;
|
|
180
|
-
if (index >= args.length) return { state: "prefix" };
|
|
181
|
-
for (const expected of token) {
|
|
182
|
-
if (index >= args.length) return { state: "prefix" };
|
|
183
|
-
if (args[index] !== expected) return { state: "none" };
|
|
184
|
-
index++;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return { state: "open", valueStart: index };
|
|
188
|
-
};
|
|
189
|
-
/**
|
|
190
|
-
* Whether a body could still grow into one complete outer Markdown fence.
|
|
191
|
-
*
|
|
192
|
-
* `stripMarkdownCodeFence` removes such a fence at completion for exactly the two tools that
|
|
193
|
-
* own the grammar, so a fenced body's streamed bytes and its completed input disagree unless
|
|
194
|
-
* the stream holds. A buffer that does not open with a fence can never acquire one, so
|
|
195
|
-
* ordinary bodies are unaffected; a buffer that does keeps its preview suppressed for the
|
|
196
|
-
* whole call, because a closing fence can still be followed by more text that withdraws it.
|
|
197
|
-
*/
|
|
198
|
-
const mayBecomeFencedBody = (text: string, toolName: string): boolean => {
|
|
199
|
-
if (toolName !== "exec" && toolName !== "apply_patch") return false;
|
|
200
|
-
const head = text.trimStart();
|
|
201
|
-
if (head === "") return true;
|
|
202
|
-
return head.startsWith("```") || "```".startsWith(head);
|
|
203
|
-
};
|
|
204
|
-
/** The two-character escapes JSON defines, and nothing else. */
|
|
205
|
-
const JSON_ESCAPES = new Map<string, string>([
|
|
206
|
-
['"', '"'], ["\\", "\\"], ["/", "/"],
|
|
207
|
-
["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
|
|
208
|
-
]);
|
|
209
|
-
const LOW_SURROGATE_ESCAPE = /^\\u[dD][c-fC-F][0-9a-fA-F]{2}$/;
|
|
210
|
-
/**
|
|
211
|
-
* The decoded prefix of a JSON string body, stopping at the first byte it cannot resolve.
|
|
212
|
-
*
|
|
213
|
-
* Every stop is a hold rather than a guess, because `JSON.parse` decides the completed value
|
|
214
|
-
* and anything invented here would be retracted. Three of them are not obvious:
|
|
215
|
-
*
|
|
216
|
-
* An escape JSON does not define makes the whole wrapper unparseable no matter what arrives
|
|
217
|
-
* next, so completion falls back to the raw text. Returning `null` for that case stops the
|
|
218
|
-
* preview rather than continuing to decode a value the completed item will never carry.
|
|
219
|
-
* `\\b` and `\\f` are defined, and were previously decoded to the letters b and f.
|
|
220
|
-
*
|
|
221
|
-
* A lone high surrogate is not a character. Emitting one alone puts an unpaired code unit in a
|
|
222
|
-
* delta the client has to decode by itself, so the pair is emitted together or not at all.
|
|
223
|
-
*/
|
|
224
|
-
const decodeJsonStringPrefix = (body: string): string | null => {
|
|
225
|
-
let out = "";
|
|
226
|
-
for (let i = 0; i < body.length; i++) {
|
|
227
|
-
const c = body[i];
|
|
228
|
-
if (c === '"') break; // unescaped closing quote: value complete
|
|
229
|
-
if (c !== "\\") { out += c; continue; }
|
|
230
|
-
const n = body[i + 1];
|
|
231
|
-
if (n === undefined) break; // escape split across chunks: wait for more
|
|
232
|
-
if (n === "u") {
|
|
233
|
-
const hex = body.slice(i + 2, i + 6);
|
|
234
|
-
if (hex.length < 4) break; // split across chunks: wait for more
|
|
235
|
-
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null; // never parses
|
|
236
|
-
const code = parseInt(hex, 16);
|
|
237
|
-
if (code >= 0xd800 && code <= 0xdbff) {
|
|
238
|
-
const low = body.slice(i + 6, i + 12);
|
|
239
|
-
if (!LOW_SURROGATE_ESCAPE.test(low)) break;
|
|
240
|
-
out += String.fromCharCode(code, parseInt(low.slice(2), 16));
|
|
241
|
-
i += 11;
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
out += String.fromCharCode(code);
|
|
245
|
-
i += 5;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
const escaped = JSON_ESCAPES.get(n);
|
|
249
|
-
if (escaped === undefined) return null; // never parses
|
|
250
|
-
out += escaped;
|
|
251
|
-
i += 1;
|
|
252
|
-
}
|
|
253
|
-
return out;
|
|
254
|
-
};
|
|
255
|
-
/**
|
|
256
|
-
* The value to stream so far, or `null` to HOLD because nothing can be decided yet.
|
|
257
|
-
*
|
|
258
|
-
* `input` is decidable from its prefix: `unwrapFreeformToolInput` returns it whenever the
|
|
259
|
-
* key is present, whatever else the object carries, so its value can be unescaped
|
|
260
|
-
* progressively and never retracted.
|
|
261
|
-
*
|
|
262
|
-
* One case escapes that claim and is accepted rather than fixed: a duplicate `input` key.
|
|
263
|
-
* `JSON.parse` keeps the last one, so `{"input":"a","input":"b"}` streams a and completes with
|
|
264
|
-
* b. Closing it means holding every canonical wrapper until its object parses, which is the
|
|
265
|
-
* progressive streaming this path exists to provide. The completed item stays authoritative,
|
|
266
|
-
* and no model emits a duplicate key in practice.
|
|
267
|
-
*
|
|
268
|
-
* A wrapper that turns invalid AFTER streaming has committed has the same shape and the same
|
|
269
|
-
* answer. `{"input":"a` followed by `\\qb"}` has already published a when the undefined escape
|
|
270
|
-
* arrives, and completion returns the raw text because nothing parses. The preview stops
|
|
271
|
-
* there: no rewind, and no decoded text the completed item does not contain. Bounding the
|
|
272
|
-
* damage is what is available without giving up progressive streaming, and the args are
|
|
273
|
-
* unusable in that case whichever representation wins.
|
|
274
|
-
*
|
|
275
|
-
* A fallback key is not. It only unwraps when it is the SINGLE string field, and a second
|
|
276
|
-
* key can still arrive — so a value emitted early would have to be taken back. That is the
|
|
277
|
-
* rewind this holds instead: stream nothing until the object closes, then publish the one
|
|
278
|
-
* repaired body. The routed passthrough in `responses-custom-tool-repair.ts` already holds
|
|
279
|
-
* any object prefix for the same reason (#5047).
|
|
280
|
-
*/
|
|
281
|
-
const freeformPartialInput = (args: string, toolName: string): string | null => {
|
|
282
|
-
const openings = ["input", ...freeformFallbackKeys(toolName)]
|
|
283
|
-
.map(key => ({ key, opening: wrapperOpening(args, key) }));
|
|
284
|
-
const canonical = openings[0]!.opening;
|
|
285
|
-
if (canonical.state === "open") {
|
|
286
|
-
const decoded = decodeJsonStringPrefix(args.slice(canonical.valueStart));
|
|
287
|
-
if (decoded === null) return null;
|
|
288
|
-
return mayBecomeFencedBody(decoded, toolName) ? null : decoded;
|
|
289
|
-
}
|
|
290
|
-
if (openings.some(entry => entry.opening.state === "open")) {
|
|
291
|
-
// Committed to a fallback wrapper. Undecidable until the object is complete.
|
|
292
|
-
try {
|
|
293
|
-
JSON.parse(args);
|
|
294
|
-
} catch {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
return unwrapFreeformToolInput(args, toolName);
|
|
298
|
-
}
|
|
299
|
-
// Still an ambiguous prefix of some wrapper: which wrapper, if any, is not known yet.
|
|
300
|
-
if (openings.some(entry => entry.opening.state === "prefix")) return null;
|
|
301
|
-
return mayBecomeFencedBody(args, toolName) ? null : args;
|
|
302
|
-
};
|
|
303
155
|
// tool_search_call carries arguments as a JSON object ({query, limit}); parse the model's arg string.
|
|
304
156
|
const parseArgsObj = (args: string): Record<string, unknown> => {
|
|
305
157
|
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
@@ -1199,7 +1051,7 @@ export function bridgeToResponsesSSE(
|
|
|
1199
1051
|
});
|
|
1200
1052
|
}
|
|
1201
1053
|
if (currentToolCall.freeform && !currentToolCall.codeModeHelperName) {
|
|
1202
|
-
// `
|
|
1054
|
+
// `progressiveFreeformInput` holds while the buffer is still an ambiguous prefix
|
|
1203
1055
|
// of a JSON wrapper; otherwise stream only the unwrapped input suffix, never
|
|
1204
1056
|
// rewinding on a mode flip.
|
|
1205
1057
|
//
|
|
@@ -1209,7 +1061,7 @@ export function bridgeToResponsesSSE(
|
|
|
1209
1061
|
// is the same disagreement in the other direction.
|
|
1210
1062
|
const ownsFreeformGrammar = currentToolCall.namespace === undefined
|
|
1211
1063
|
|| currentToolCall.namespace === "functions";
|
|
1212
|
-
const full =
|
|
1064
|
+
const full = progressiveFreeformInput(
|
|
1213
1065
|
currentToolCall.args,
|
|
1214
1066
|
ownsFreeformGrammar ? currentToolCall.name : "",
|
|
1215
1067
|
);
|
|
@@ -403,16 +403,16 @@ export async function cmdAutoSwitch(args: string[], deps: AccountDeps): Promise<
|
|
|
403
403
|
// shared kernel, and absent is a server that does not speak this field at all. Collapsing
|
|
404
404
|
// false into absent would render the live feature as an unknown capability.
|
|
405
405
|
const inert = typeof settings.inert === "boolean" ? settings.inert : null;
|
|
406
|
-
// A stored threshold only steers selection once the pool consumes it, which is
|
|
407
|
-
// what `inert: false` reports.
|
|
408
|
-
const enabled = inert === false && storedThreshold !== null;
|
|
406
|
+
// A positive stored threshold only steers selection once the pool consumes it, which is
|
|
407
|
+
// exactly what `inert: false` reports. Zero remains the explicit disabled value.
|
|
408
|
+
const enabled = inert === false && storedThreshold !== null && storedThreshold > 0;
|
|
409
409
|
if (wantsJson) {
|
|
410
410
|
console.log(JSON.stringify({ provider: name, autoSwitchThreshold: storedThreshold, enabled, poolEnabled, inert }, null, 2));
|
|
411
411
|
} else {
|
|
412
412
|
const value = storedThreshold === null ? "unset" : `${storedThreshold}%`;
|
|
413
413
|
const state = inert === false ? (enabled ? "on" : "off") : inert === true ? "inactive" : "unavailable";
|
|
414
414
|
const why = inert === false
|
|
415
|
-
? (enabled ? "applied by this pool" : "no threshold stored")
|
|
415
|
+
? (enabled ? "applied by this pool" : storedThreshold === 0 ? "usage-based switching disabled" : "no threshold stored")
|
|
416
416
|
: inert === true ? "not applied by this pool" : "threshold support is unknown";
|
|
417
417
|
console.log(`auto-switch: ${state} (stored threshold ${value}; ${why})`);
|
|
418
418
|
}
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -945,9 +945,9 @@ export type StartOwnerDecision = "refuse" | "service-stay-out" | "sibling";
|
|
|
945
945
|
*
|
|
946
946
|
* The #3106 guard exists so a bare `start` cannot shadow a healthy configured-port
|
|
947
947
|
* proxy with an ephemeral-port copy. An interactive `--port X` naming a DIFFERENT
|
|
948
|
-
* port than the live proxy's is an explicit sibling request, not that shadow
|
|
949
|
-
*
|
|
950
|
-
*
|
|
948
|
+
* port than the live proxy's is an explicit sibling request, not that shadow. The
|
|
949
|
+
* state-directory spend-ledger lease makes the final same-home refusal; keeping this
|
|
950
|
+
* decision allows isolated homes on one machine to remain independent.
|
|
951
951
|
* The service wrapper always passes the configured port and keeps its exact
|
|
952
952
|
* stay-out-of-the-way semantics: it never takes the sibling path.
|
|
953
953
|
*/
|
package/src/cli/doctor.ts
CHANGED
|
@@ -1505,8 +1505,16 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1505
1505
|
// cannot break a legitimately green pipeline.
|
|
1506
1506
|
|
|
1507
1507
|
console.log("\nCodex agent role files");
|
|
1508
|
-
|
|
1509
|
-
|
|
1508
|
+
let roleScanError: unknown;
|
|
1509
|
+
const tomlFallbackRoles = scanCodexAgentRolesWithTomlModelFallback(
|
|
1510
|
+
resolveCodexHomeDirImpl(),
|
|
1511
|
+
cause => {
|
|
1512
|
+
roleScanError = cause;
|
|
1513
|
+
},
|
|
1514
|
+
);
|
|
1515
|
+
if (roleScanError) {
|
|
1516
|
+
console.log(` [WARN] unable to scan $CODEX_HOME/agents/*.toml: ${String(roleScanError)}`);
|
|
1517
|
+
} else if (tomlFallbackRoles.length === 0) {
|
|
1510
1518
|
console.log(" ok no per-role model_fallback fields in $CODEX_HOME/agents/*.toml");
|
|
1511
1519
|
} else {
|
|
1512
1520
|
console.log(` [WARN] ${tomlFallbackRoles.length} agent role file${tomlFallbackRoles.length === 1 ? "" : "s"} contain${tomlFallbackRoles.length === 1 ? "s" : ""} \`model_fallback\`: ${tomlFallbackRoles.join(", ")}`);
|
|
@@ -1514,13 +1522,24 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1514
1522
|
}
|
|
1515
1523
|
// opencodex does not write these files; the Codex desktop external-agent import does, and it
|
|
1516
1524
|
// drops the model pin on the way in. Observe-only: doctor never repairs or removes them.
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1525
|
+
// Both role scans share the same directory listing, so a failure above already reported the
|
|
1526
|
+
// cause; skip the second scan rather than warn twice or print a false "ok".
|
|
1527
|
+
if (!roleScanError) {
|
|
1528
|
+
const unpinnedDerivedRoles = scanOpencodexDerivedCodexAgentRolesWithoutModelPin(
|
|
1529
|
+
resolveCodexHomeDirImpl(),
|
|
1530
|
+
cause => {
|
|
1531
|
+
roleScanError = cause;
|
|
1532
|
+
},
|
|
1533
|
+
);
|
|
1534
|
+
if (roleScanError) {
|
|
1535
|
+
console.log(` [WARN] unable to scan $CODEX_HOME/agents/*.toml: ${String(roleScanError)}`);
|
|
1536
|
+
} else if (unpinnedDerivedRoles.length === 0) {
|
|
1537
|
+
console.log(" ok every opencodex-derived role file in $CODEX_HOME/agents/*.toml pins a model");
|
|
1538
|
+
} else {
|
|
1539
|
+
console.log(` [WARN] ${unpinnedDerivedRoles.length} opencodex-derived role file${unpinnedDerivedRoles.length === 1 ? "" : "s"} without a \`model\` pin: ${unpinnedDerivedRoles.map(role => `${role}.toml`).join(", ")}`);
|
|
1540
|
+
console.log(" Codex runs these roles on the parent model, so a spawn records one role and another model. The `ocx-route` directive in the file cannot pin them: it is honoured only on the Claude Code `/v1/messages` path and is inert on `/v1/responses`.");
|
|
1541
|
+
console.log(" Add `model = \"<id>\"` to each file, or remove them. They usually come from the Codex desktop external-agent import of ~/.claude/agents/ocx-*.md; set `[desktop] external-agent-import-sync-item-types` with `SUBAGENTS = false` to stop it recreating them.");
|
|
1542
|
+
}
|
|
1524
1543
|
}
|
|
1525
1544
|
|
|
1526
1545
|
const dual = collectWslDualInstall();
|
package/src/cli/hub.ts
CHANGED
|
@@ -228,8 +228,9 @@ export function hubInviteCommand(
|
|
|
228
228
|
managementUrl: string,
|
|
229
229
|
clients: OcxConnectedClientId[],
|
|
230
230
|
): string {
|
|
231
|
-
const
|
|
232
|
-
|
|
231
|
+
const shellQuote = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
232
|
+
const clientsFlag = clients.length > 0 ? ` --clients ${shellQuote(clients.join(","))}` : "";
|
|
233
|
+
return `echo ${shellQuote(code)} | ocx connect ${shellQuote(dataUrl)} --management-url ${shellQuote(managementUrl)}${clientsFlag} --pairing-code-stdin`;
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
async function runInvite(args: string[], deps: HubCommandDeps): Promise<number> {
|
package/src/cli/index.ts
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
} from "./tray-proxy";
|
|
79
79
|
import { requestBoundSystemRestart } from "./system-restart-client";
|
|
80
80
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
81
|
+
import { SpendLedgerOwnerError } from "../lib/spend-ledger-owner";
|
|
81
82
|
import { redactUrlForLog } from "../lib/redact";
|
|
82
83
|
import { dispatchCommand, decideBusyPreferredPort, decideStartWithLiveOwner } from "./dispatch";
|
|
83
84
|
import { AuxiliaryListenerBindError, findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
|
|
@@ -418,8 +419,8 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
418
419
|
// to bake the service (observed: a probe on 10198 left the service pinned there).
|
|
419
420
|
siblingStart = true;
|
|
420
421
|
console.warn(
|
|
421
|
-
`Proxy already running on port ${owner.live.port};
|
|
422
|
-
+ `
|
|
422
|
+
`Proxy already running on port ${owner.live.port}; requested a second instance on port ${requestedPort}. `
|
|
423
|
+
+ `Startup continues only for an independent OPENCODEX_HOME; one state directory has one spend-ledger writer.`,
|
|
423
424
|
);
|
|
424
425
|
}
|
|
425
426
|
|
|
@@ -463,6 +464,10 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
463
464
|
scheduleCatalogPrewarm();
|
|
464
465
|
break;
|
|
465
466
|
} catch (err) {
|
|
467
|
+
if (err instanceof SpendLedgerOwnerError) {
|
|
468
|
+
console.error(`❌ ${err.message}`);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
466
471
|
if (err instanceof AuxiliaryListenerBindError || !isAddrInUse(err) || attempt >= 2) throw err;
|
|
467
472
|
if (requestedPort !== undefined) {
|
|
468
473
|
console.log(`⚠️ Port ${port} was taken while starting; waiting to retry the same port...`);
|
package/src/cli/opencode.ts
CHANGED
|
@@ -41,7 +41,7 @@ import type {
|
|
|
41
41
|
} from "../clients/config-export";
|
|
42
42
|
import { filterCatalogVisibleModels, visibleNativeSlugs } from "../codex/catalog";
|
|
43
43
|
import { commandInvocation } from "../lib/win-exec";
|
|
44
|
-
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
44
|
+
import { configuredAdminToken, opencodeCatalogToken } from "../lib/admin-secrets";
|
|
45
45
|
import { localManagementOrigin } from "../lib/local-destinations";
|
|
46
46
|
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
47
47
|
import { loadServiceTokenFromFile, serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
@@ -684,7 +684,7 @@ export async function cmdOpencode(args: string[]): Promise<number> {
|
|
|
684
684
|
try {
|
|
685
685
|
const managementToken = configuredAdminToken();
|
|
686
686
|
if (!managementToken) throw new Error("No local admin token is available; check the running proxy's home.");
|
|
687
|
-
proxyModels = await fetchOpencodeProxyModels(live, managementToken, {
|
|
687
|
+
proxyModels = await fetchOpencodeProxyModels(live, opencodeCatalogToken(managementToken), {
|
|
688
688
|
managementOrigin: localManagementOrigin({ ...startupConfig, hostname: live.hostname }, live.port),
|
|
689
689
|
});
|
|
690
690
|
} catch (error) {
|