@bitkyc08/opencodex 2.68.0 → 2.69.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/bin/ocx.mjs +25 -17
- package/gui/dist/assets/App-DDbgOTh6.js +51 -0
- package/gui/dist/assets/{Tray-Bn8WhKBH.js → Tray-uqKRqfvw.js} +1 -1
- package/gui/dist/assets/index-BF9QnKO_.css +1 -0
- package/gui/dist/assets/index-CuaBJkG4.js +86 -0
- package/gui/dist/assets/{quota-summary-DL-bNuNL.js → quota-summary-COppQSVq.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/factory-droid.svg +8 -0
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +109 -11
- package/src/adapters/coding-agent/protocol.ts +106 -11
- package/src/adapters/coding-agent/turn.ts +23 -18
- package/src/adapters/google-errors.ts +16 -1
- package/src/adapters/google-http.ts +14 -0
- package/src/adapters/openai-chat/passthrough.ts +2 -0
- package/src/adapters/openai-chat/summary-budget.ts +72 -0
- package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
- package/src/adapters/openai-chat.ts +6 -9
- package/src/bridge/response-json.ts +3 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/desktop-picker.ts +6 -2
- package/src/claude/desktop-system-proxy.ts +155 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
- package/src/claude/intercept/picker-ca.ts +283 -83
- package/src/claude/intercept/picker-models.ts +4 -1
- package/src/claude/intercept/picker-trust.ts +15 -2
- package/src/claude/intercept/runtime.ts +99 -32
- package/src/cli/account-api.ts +5 -2
- package/src/cli/account-extended.ts +47 -11
- package/src/cli/account-target.ts +12 -7
- package/src/cli/account.ts +47 -3
- package/src/cli/capabilities.ts +33 -7
- package/src/cli/claude-agent-startup-sync.ts +2 -0
- package/src/cli/claude-desktop.ts +37 -13
- package/src/cli/cross-home-owner.ts +138 -0
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +18 -8
- package/src/cli/ensure-desired-integrations.ts +5 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +15 -18
- package/src/cli/integrations.ts +8 -1
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +106 -4
- package/src/cli/models.ts +6 -1
- package/src/cli/opencode.ts +2 -83
- package/src/cli/registry.ts +3 -2
- package/src/cli/status.ts +86 -7
- package/src/client/connect.ts +24 -10
- package/src/client/link-ingress.ts +3 -1
- package/src/client/link-join.ts +76 -9
- package/src/client/link-relay-transport.ts +184 -0
- package/src/client/link-relay.ts +16 -4
- package/src/client/machine-listener.ts +2 -0
- package/src/clients/config-export/constants.ts +8 -0
- package/src/clients/config-export/contracts.ts +13 -1
- package/src/clients/config-export/droid.ts +71 -0
- package/src/clients/config-export/kilo.ts +116 -0
- package/src/clients/config-export.ts +33 -2
- package/src/codex/auth-api/routes.ts +1 -1
- package/src/codex/catalog/access-programs.ts +14 -2
- package/src/codex/catalog/provider-models.ts +1 -1
- package/src/codex/catalog-auto-refresh.ts +122 -0
- package/src/codex/config-drift-heal.ts +80 -0
- package/src/codex/desktop-switches.ts +7 -5
- package/src/codex/inject/bounded-config-reader.ts +69 -0
- package/src/codex/inject/config-toml.ts +31 -3
- package/src/codex/inject/routing-target.ts +6 -1
- package/src/codex/inject.ts +4 -0
- package/src/codex/model-entitlements.ts +32 -5
- package/src/codex/native-profile-startup.ts +17 -5
- package/src/codex/project-config-warnings.ts +72 -21
- package/src/codex/quota-auto-refresh-state.ts +8 -2
- package/src/codex/quota-auto-refresh.ts +80 -9
- package/src/codex/sync.ts +10 -0
- package/src/combos/failover.ts +4 -1
- package/src/combos/jev.ts +15 -4
- package/src/combos/types.ts +16 -0
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +29 -0
- package/src/config/macos-system-proxy.ts +114 -0
- package/src/config/proxy-env.ts +67 -20
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +23 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +220 -144
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +12 -3
- package/src/integrations/droid-settings.ts +65 -0
- package/src/integrations/kilo-candidates.ts +30 -0
- package/src/integrations/mutation-plan.ts +62 -7
- package/src/integrations/native/ownership-preflight.ts +46 -12
- package/src/integrations/registry.ts +136 -7
- package/src/integrations/state.ts +52 -4
- package/src/integrations/target.ts +3 -0
- package/src/integrations/writer.ts +55 -14
- package/src/lib/account-selection-events.ts +14 -0
- package/src/lib/jsonc.ts +110 -0
- package/src/lib/standalone.ts +9 -1
- package/src/lib/windows-system-proxy.ts +101 -1
- package/src/link/relay-auth.ts +29 -0
- package/src/link/ssh-argv.ts +6 -3
- package/src/link/ssh-runner.ts +5 -5
- package/src/oauth/generic-account-failover.ts +52 -8
- package/src/oauth/index.ts +37 -14
- package/src/oauth/store.ts +96 -11
- package/src/oauth/token-guardian.ts +7 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +5 -3
- package/src/providers/fastwire.ts +3 -0
- package/src/providers/key-failover.ts +13 -5
- package/src/providers/kiro-model-catalog.ts +4 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-tiers-destination.ts +9 -0
- package/src/providers/quota/account-cache.ts +17 -3
- package/src/providers/quota/vendor-probes-oauth.ts +2 -0
- package/src/providers/quota.ts +2 -2
- package/src/responses/code-mode-helper-compat.ts +14 -1
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/images.ts +70 -20
- package/src/server/index/link-listener.ts +20 -3
- package/src/server/index/link-relay-sessions.ts +138 -0
- package/src/server/index/optional-listeners.ts +11 -1
- package/src/server/index/serve-options.ts +12 -0
- package/src/server/inference/client-encoder-delivery.ts +1 -0
- package/src/server/local-management-read-client.ts +21 -2
- package/src/server/management/agent-settings-routes.ts +19 -5
- package/src/server/management/combo-routes.ts +5 -3
- package/src/server/management/config-routes.ts +32 -6
- package/src/server/management/model-routes.ts +232 -7
- package/src/server/management/model-rows.ts +199 -1
- package/src/server/management/oauth-account-routes.ts +63 -3
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/port-reclaim.ts +170 -32
- package/src/server/responses/adapter-delivery.ts +6 -4
- package/src/server/responses/adapter-dispatch.ts +72 -2
- package/src/server/responses/core-combo.ts +6 -1
- package/src/server/responses/core-normalize.ts +19 -4
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/memory-models.ts +169 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +10 -0
- package/src/server/responses/request-prepare.ts +86 -4
- package/src/server/responses/request-transport.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +5 -2
- package/src/server/responses/shadow-target-availability.ts +17 -4
- package/src/server/responses/sidecar-execution.ts +18 -2
- package/src/server/responses/skills-snapshot.ts +239 -0
- package/src/server/responses-undeclared-tool-guard.ts +43 -6
- package/src/server/search.ts +1 -1
- package/src/service/guards.ts +4 -5
- package/src/service/state.ts +39 -2
- package/src/service.ts +2 -2
- package/src/types/config.ts +46 -4
- package/src/types/provider.ts +9 -0
- package/src/types/request.ts +6 -0
- package/src/types/tools.ts +38 -8
- package/src/types.ts +3 -0
- package/src/update/async-check.ts +5 -1
- package/src/update/index.ts +30 -12
- package/src/update/job.ts +3 -1
- package/src/update/pnpm-read-policy.d.mts +4 -0
- package/src/update/pnpm-read-policy.mjs +44 -0
- package/src/update/worker-launch.ts +152 -11
- package/src/usage/cost.ts +6 -3
- package/src/usage/log.ts +4 -0
- package/src/web-search/backends.ts +2 -2
- package/src/web-search/devin-executor.ts +1 -1
- package/src/web-search/sidecar-providers.ts +5 -5
- package/gui/dist/assets/App-mpsibsFj.js +0 -51
- package/gui/dist/assets/index-BDUBS8PW.css +0 -1
- package/gui/dist/assets/index-kUE8tuAK.js +0 -86
|
@@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types";
|
|
6
|
+
import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
|
|
6
7
|
import { commandInvocation } from "../../lib/win-exec";
|
|
7
8
|
import { isStandaloneBinary } from "../../lib/standalone";
|
|
8
9
|
import { modelRecordValue } from "../../reasoning-effort";
|
|
@@ -10,9 +11,11 @@ import type { IncomingMeta } from "../base";
|
|
|
10
11
|
import {
|
|
11
12
|
buildConversationInput,
|
|
12
13
|
CodingAgentProtocolError,
|
|
14
|
+
MAX_TOOL_BLOCK_STARTS,
|
|
13
15
|
mapStreamMessageToEvents,
|
|
14
16
|
projectedHistoryCharLimit,
|
|
15
17
|
readJsonLines,
|
|
18
|
+
releaseOpenToolBlocks,
|
|
16
19
|
toolBridgeInitError,
|
|
17
20
|
type StreamParseState,
|
|
18
21
|
} from "./protocol";
|
|
@@ -369,6 +372,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
369
372
|
};
|
|
370
373
|
|
|
371
374
|
let streamProtocolError: string | undefined;
|
|
375
|
+
let streamProtocolCode: string | undefined;
|
|
372
376
|
let turnError: string | undefined;
|
|
373
377
|
// A successful result frame that arrived after every captured tool call completed but
|
|
374
378
|
// before message_stop. The bridge contract still ends the leg with the synthesized
|
|
@@ -381,6 +385,9 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
381
385
|
sawPartialThinking: false,
|
|
382
386
|
sawTerminalResult: false,
|
|
383
387
|
openToolBlocks: new Map(),
|
|
388
|
+
translatorBudget: incoming.translatorBudget,
|
|
389
|
+
maxToolBlockStarts: toolBridge?.maxTurnToolCalls,
|
|
390
|
+
strictToolBlockCapture: Boolean(toolBridge),
|
|
384
391
|
partialToolCallIds: toolBridge ? new Set<string>() : undefined,
|
|
385
392
|
};
|
|
386
393
|
|
|
@@ -403,7 +410,6 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
403
410
|
if (!stdout) throw new CodingAgentProtocolError(`${profile.label} CLI produced no stdout stream`);
|
|
404
411
|
try {
|
|
405
412
|
let initValidated = false;
|
|
406
|
-
let toolCallStarts = 0;
|
|
407
413
|
let failClosed = false;
|
|
408
414
|
for await (const message of readJsonLines(stdout)) {
|
|
409
415
|
if (incoming.abortSignal?.aborted) break;
|
|
@@ -447,6 +453,18 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
447
453
|
}
|
|
448
454
|
}
|
|
449
455
|
const mappedEvents = mapStreamMessageToEvents(message, state);
|
|
456
|
+
if (state.toolCallLimitExceeded) {
|
|
457
|
+
emitOnce({
|
|
458
|
+
type: "error",
|
|
459
|
+
message: `Coding-agent CLI returned more than the ${Math.min(MAX_TOOL_BLOCK_STARTS, state.maxToolBlockStarts ?? MAX_TOOL_BLOCK_STARTS)}-tool-call turn limit.`,
|
|
460
|
+
status: 502,
|
|
461
|
+
errorType: "upstream_error",
|
|
462
|
+
code: "tool_call_limit",
|
|
463
|
+
retryable: false,
|
|
464
|
+
});
|
|
465
|
+
kill();
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
450
468
|
if (toolBridge && state.uncapturedToolUse) {
|
|
451
469
|
emitOnce({
|
|
452
470
|
type: "error",
|
|
@@ -474,20 +492,6 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
474
492
|
break;
|
|
475
493
|
}
|
|
476
494
|
if (toolBridge && event.type === "tool_call_start") {
|
|
477
|
-
toolCallStarts += 1;
|
|
478
|
-
if (toolCallStarts > toolBridge.maxTurnToolCalls) {
|
|
479
|
-
emitOnce({
|
|
480
|
-
type: "error",
|
|
481
|
-
message: `Coding-agent CLI returned more than the ${toolBridge.maxTurnToolCalls}-tool-call turn limit.`,
|
|
482
|
-
status: 502,
|
|
483
|
-
errorType: "upstream_error",
|
|
484
|
-
code: "tool_call_limit",
|
|
485
|
-
retryable: false,
|
|
486
|
-
});
|
|
487
|
-
failClosed = true;
|
|
488
|
-
kill();
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
495
|
const wireName = toolBridge.emittedNameMap.get(event.name);
|
|
492
496
|
if (wireName === undefined) {
|
|
493
497
|
emitOnce({
|
|
@@ -593,8 +597,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
593
597
|
break;
|
|
594
598
|
}
|
|
595
599
|
if (toolBridge && !terminalEmitted && state.sawMessageStop && (state.completedToolCalls ?? 0) > 0) {
|
|
596
|
-
//
|
|
597
|
-
// after the bridge init handshake.
|
|
600
|
+
// The raw-start gate above already refused any call opened before the handshake.
|
|
598
601
|
// The capture-only MCP handler never answers, so the CLI parks after message_stop.
|
|
599
602
|
// The completed tool_use blocks are this turn's structured output: end the leg here
|
|
600
603
|
// and terminate the tree; the client executes, and the next request continues.
|
|
@@ -615,11 +618,13 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
615
618
|
} catch (err) {
|
|
616
619
|
kill();
|
|
617
620
|
streamProtocolError = err instanceof Error ? err.message : String(err);
|
|
621
|
+
if (isTranslatorBudgetExceededError(err)) streamProtocolCode = err.code;
|
|
618
622
|
}
|
|
619
623
|
} catch (err) {
|
|
620
624
|
kill();
|
|
621
625
|
turnError = err instanceof Error ? err.message : String(err);
|
|
622
626
|
} finally {
|
|
627
|
+
releaseOpenToolBlocks(state);
|
|
623
628
|
cleanup();
|
|
624
629
|
if (toolBridgeDir) {
|
|
625
630
|
await rm(toolBridgeDir, { recursive: true, force: true }).catch(() => undefined);
|
|
@@ -666,7 +671,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
666
671
|
message: redactSecrets(streamProtocolError, profile.tokenEnv, apiKey),
|
|
667
672
|
status: 502,
|
|
668
673
|
errorType: "upstream_error",
|
|
669
|
-
code: "protocol_error",
|
|
674
|
+
code: streamProtocolCode ?? "protocol_error",
|
|
670
675
|
retryable: false,
|
|
671
676
|
});
|
|
672
677
|
} else if (child.exitCode !== null && child.exitCode !== 0) {
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErrorText } from "./upstream-http-error";
|
|
2
2
|
import { isLocationUnsupportedMessage } from "../lib/errors";
|
|
3
3
|
|
|
4
|
+
export const ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX = "Antigravity account validation required (VALIDATION_REQUIRED)";
|
|
5
|
+
|
|
6
|
+
function hasAntigravityValidationReason(payloadText: string): boolean {
|
|
7
|
+
const payload = parseUpstreamJsonPayload(payloadText);
|
|
8
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
9
|
+
const error = (payload as { error?: unknown }).error;
|
|
10
|
+
if (!error || typeof error !== "object" || Array.isArray(error)) return false;
|
|
11
|
+
const details = (error as { details?: unknown }).details;
|
|
12
|
+
return Array.isArray(details) && details.some(detail =>
|
|
13
|
+
detail !== null && typeof detail === "object" && !Array.isArray(detail)
|
|
14
|
+
&& (detail as { reason?: unknown }).reason === "VALIDATION_REQUIRED");
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
/** Pull the human detail out of the Google API error envelope `{error:{message,status,code}}`. */
|
|
5
18
|
function googleErrorDetail(payloadText: string): { message?: string; status?: string } {
|
|
6
19
|
const trimmed = payloadText.trim();
|
|
@@ -98,7 +111,9 @@ function classifyGoogle(label: string, status: number | undefined, enumStatus: s
|
|
|
98
111
|
*/
|
|
99
112
|
export function safeGoogleHttpErrorMessage(label: string, status: number, payloadText: string): string {
|
|
100
113
|
const { message, status: enumStatus } = googleErrorDetail(payloadText);
|
|
101
|
-
const prefix =
|
|
114
|
+
const prefix = label === "Antigravity" && status === 403 && hasAntigravityValidationReason(payloadText)
|
|
115
|
+
? ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX
|
|
116
|
+
: classifyGoogle(label, status, enumStatus, [message, enumStatus].filter(Boolean).join(" "));
|
|
102
117
|
const detail = message ? sanitizeUpstreamErrorText(message).slice(0, 500) : `HTTP ${status}`;
|
|
103
118
|
return `${prefix}: ${detail}`;
|
|
104
119
|
}
|
|
@@ -7,6 +7,7 @@ import { isQuotaExhaustedBody, retryableGoogleStatus, safeGoogleHttpErrorMessage
|
|
|
7
7
|
import { repairGoogleInvalidRequestBodyWithReport } from "./google-wire-compiler";
|
|
8
8
|
import type { GoogleToolSchemaPolicy, GoogleToolSchemaProfile } from "./google-tool-schema";
|
|
9
9
|
import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error";
|
|
10
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
10
11
|
import {
|
|
11
12
|
abortError,
|
|
12
13
|
cancelResponseBodyBestEffort,
|
|
@@ -29,6 +30,19 @@ export interface GoogleRetryOptions {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise<Response> {
|
|
33
|
+
if (label === "Antigravity" && res.status === 403) {
|
|
34
|
+
const body = await readBoundedResponseBody(res, {
|
|
35
|
+
maxBytes: 4096, totalTimeoutMs: 2000, firstByteTimeoutMs: 2000,
|
|
36
|
+
inactivityTimeoutMs: 2000, signal,
|
|
37
|
+
});
|
|
38
|
+
const headers = new Headers(res.headers);
|
|
39
|
+
headers.delete("content-encoding");
|
|
40
|
+
headers.delete("content-length");
|
|
41
|
+
return new Response(safeGoogleHttpErrorMessage(label, res.status,
|
|
42
|
+
body.displaySafe && !body.truncated ? body.text : ""), {
|
|
43
|
+
status: res.status, statusText: res.statusText, headers,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
32
46
|
return normalizeUpstreamHttpErrorResponse(res, {
|
|
33
47
|
signal,
|
|
34
48
|
formatMessage: payloadText => safeGoogleHttpErrorMessage(label, res.status, payloadText),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { protectGlmSummaryBudget } from "./summary-budget";
|
|
1
2
|
import { openAIChatTransport, stripBracketedModelSuffix } from "./wire";
|
|
2
3
|
import type { AdapterRequest } from "../base";
|
|
3
4
|
import { frameAgentRouterMessages } from "../agentrouter";
|
|
@@ -72,6 +73,7 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
72
73
|
for (const field of CHAT_PASSTHROUGH_FIELDS) {
|
|
73
74
|
if (rawBody[field] !== undefined) body[field] = rawBody[field];
|
|
74
75
|
}
|
|
76
|
+
if (protectGlmSummaryBudget(body, provider.baseUrl, body.reasoning_effort)) body.reasoning_effort = "low";
|
|
75
77
|
const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
|
|
76
78
|
const reasoningDisabled = modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0;
|
|
77
79
|
if (reasoningDisabled) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { modelRecordValue } from "../../reasoning-effort";
|
|
2
|
+
import type { OcxParsedRequest, OcxProviderConfig } from "../../types";
|
|
3
|
+
|
|
4
|
+
export function resolveMaxTokens(provider: OcxProviderConfig, parsed: OcxParsedRequest): number | undefined {
|
|
5
|
+
return parsed.options.maxOutputTokens
|
|
6
|
+
?? modelRecordValue(provider.modelMaxOutputTokens, parsed.modelId)
|
|
7
|
+
?? provider.defaultMaxOutputTokens;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function textContent(value: unknown): string | undefined {
|
|
11
|
+
if (typeof value === "string") return value;
|
|
12
|
+
if (!Array.isArray(value)) return undefined;
|
|
13
|
+
const text: string[] = [];
|
|
14
|
+
for (const part of value) {
|
|
15
|
+
if (!part || part.type !== "text" || typeof part.text !== "string") return undefined;
|
|
16
|
+
text.push(part.text);
|
|
17
|
+
}
|
|
18
|
+
return text.join("\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PROTECTED_SUMMARY_CAP = 8192;
|
|
22
|
+
/** A real checkpoint carries the conversation it summarizes; a short probe is not one (#5465). */
|
|
23
|
+
const MIN_CHECKPOINT_TRANSCRIPT_CHARS = 2000;
|
|
24
|
+
|
|
25
|
+
/** The mitigation is scoped to Z.AI's own endpoints; another gateway serving the same model id is not. */
|
|
26
|
+
function isZaiEndpoint(baseUrl: string | undefined): boolean {
|
|
27
|
+
if (!baseUrl) return false;
|
|
28
|
+
try {
|
|
29
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
30
|
+
return host === "z.ai" || host.endsWith(".z.ai");
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isTinyCap(value: unknown): value is number {
|
|
37
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 1024;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Aside's emergency checkpoint is a standalone summary, not an ordinary short answer (#5465).
|
|
42
|
+
* Runs at the physical Chat destination, after all combo effort overrides, so `effort` is the
|
|
43
|
+
* effective effort. Only the exhausting tiers (`high`/`max`) on Z.AI's GLM-5.3-Flash, for the
|
|
44
|
+
* two-message checkpoint shape with a real `<conversation>` transcript, qualify. Each tiny cap
|
|
45
|
+
* field is raised on its own, so a caller's larger cap is never shrunk.
|
|
46
|
+
*/
|
|
47
|
+
export function protectGlmSummaryBudget(
|
|
48
|
+
body: Record<string, unknown>,
|
|
49
|
+
baseUrl: string | undefined,
|
|
50
|
+
effort: unknown,
|
|
51
|
+
): boolean {
|
|
52
|
+
if (!isZaiEndpoint(baseUrl)) return false;
|
|
53
|
+
if (effort !== "high" && effort !== "max") return false;
|
|
54
|
+
if (typeof body.model !== "string"
|
|
55
|
+
|| !/^(?:(?:zai|z-ai|zai-org)\/)?glm-5\.3-flash$/i.test(body.model)) return false;
|
|
56
|
+
if (body.tools !== undefined && (!Array.isArray(body.tools) || body.tools.length > 0)) return false;
|
|
57
|
+
if (!isTinyCap(body.max_tokens) && !isTinyCap(body.max_completion_tokens)) return false;
|
|
58
|
+
const messages = body.messages;
|
|
59
|
+
if (!Array.isArray(messages) || messages.length !== 2) return false;
|
|
60
|
+
const [system, user] = messages;
|
|
61
|
+
if (!system || !user || !["system", "developer"].includes(system.role) || user.role !== "user"
|
|
62
|
+
|| system.tool_calls || user.tool_calls || system.function_call || user.function_call) return false;
|
|
63
|
+
const instruction = textContent(system.content);
|
|
64
|
+
const transcript = textContent(user.content);
|
|
65
|
+
if (instruction === undefined || transcript === undefined) return false;
|
|
66
|
+
if (!/\b(?:summari[sz](?:e|ation|er|ing)|summary|checkpoint)\b/i.test(instruction)) return false;
|
|
67
|
+
if (!/<conversation>[\s\S]*<\/conversation>/i.test(transcript)) return false;
|
|
68
|
+
if (transcript.length < MIN_CHECKPOINT_TRANSCRIPT_CHARS) return false;
|
|
69
|
+
if (isTinyCap(body.max_tokens)) body.max_tokens = PROTECTED_SUMMARY_CAP;
|
|
70
|
+
if (isTinyCap(body.max_completion_tokens)) body.max_completion_tokens = PROTECTED_SUMMARY_CAP;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
@@ -17,6 +17,7 @@ import type { OcxMessage } from "../../types";
|
|
|
17
17
|
*/
|
|
18
18
|
export function createToolCallIdReminter(reservedIds: Iterable<string>): (rawId: string) => string {
|
|
19
19
|
const occupied = new Set(reservedIds);
|
|
20
|
+
const nextSuffixByWidthAndPrefix = new Map<string, number>();
|
|
20
21
|
return rawId => {
|
|
21
22
|
if (!occupied.has(rawId)) {
|
|
22
23
|
occupied.add(rawId);
|
|
@@ -25,16 +26,24 @@ export function createToolCallIdReminter(reservedIds: Iterable<string>): (rawId:
|
|
|
25
26
|
// A non-conforming source is sanitized, never dropped: the wire still needs an id, and the
|
|
26
27
|
// occupied check below covers a sanitized form that now equals some other call's id.
|
|
27
28
|
const base = isConformingToolCallId(rawId) ? rawId : rawId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
28
|
-
for (let n = 2; ;
|
|
29
|
+
for (let n = 2; ;) {
|
|
29
30
|
// Hyphen, not underscore: an id that extends another id as `<earlier>_<digits>` is parsed by
|
|
30
31
|
// at least one client as a batch sub-call of `<earlier>`, which pairs the second call's
|
|
31
32
|
// result to the first call. A `-<n>` suffix is in the same id family without that reading.
|
|
32
33
|
const suffix = `-${n}`;
|
|
33
|
-
|
|
34
|
+
// A wider suffix retains less of the base, so siblings that were separate at -9
|
|
35
|
+
// can converge at -10. Resume in the candidate's actual width/prefix domain.
|
|
36
|
+
const prefix = base.slice(0, Math.max(1, MAX_TOOL_CALL_ID_LENGTH - suffix.length));
|
|
37
|
+
const cursorKey = `${suffix.length}:${prefix}`;
|
|
38
|
+
const next = nextSuffixByWidthAndPrefix.get(cursorKey);
|
|
39
|
+
if (next !== undefined && next > n) { n = next; continue; }
|
|
40
|
+
const candidate = prefix + suffix;
|
|
41
|
+
nextSuffixByWidthAndPrefix.set(cursorKey, n + 1);
|
|
34
42
|
if (!occupied.has(candidate)) {
|
|
35
43
|
occupied.add(candidate);
|
|
36
44
|
return candidate;
|
|
37
45
|
}
|
|
46
|
+
n++;
|
|
38
47
|
}
|
|
39
48
|
};
|
|
40
49
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { hasShrinkableOpenAIChatImages, normalizeOpenAIChatImages } from "./openai-chat-images";
|
|
2
|
+
import { protectGlmSummaryBudget, resolveMaxTokens } from "./openai-chat/summary-budget";
|
|
2
3
|
import { chatParallelToolCallsWireValue } from "./openai-chat/parallel-tool-calls";
|
|
3
4
|
import { applyExplicitChatReasoningWirePolicy } from "./openai-chat/reasoning-wire";
|
|
4
5
|
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
|
|
5
6
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
|
|
6
7
|
import { modelInList } from "../types";
|
|
7
8
|
import { createInlineThinkContentSplitter, splitInlineThinkContent } from "./inline-think-tags";
|
|
8
|
-
import { mapReasoningEffort
|
|
9
|
+
import { mapReasoningEffort } from "../reasoning-effort";
|
|
9
10
|
import { debugProviderDiagnostic } from "../lib/debug";
|
|
10
11
|
import { sseFieldValue } from "../lib/sse-decoder";
|
|
11
12
|
import { isDebugEnabled } from "../lib/debug-settings";
|
|
@@ -51,12 +52,6 @@ export { stripBracketedModelSuffix } from "./openai-chat/wire";
|
|
|
51
52
|
export { buildOpenAIChatPassthroughRequest } from "./openai-chat/passthrough";
|
|
52
53
|
export { formatOpenAIChatErrorBody } from "./openai-chat/errors";
|
|
53
54
|
|
|
54
|
-
function resolveMaxTokens(provider: OcxProviderConfig, parsed: OcxParsedRequest): number | undefined {
|
|
55
|
-
return parsed.options.maxOutputTokens
|
|
56
|
-
?? modelRecordValue(provider.modelMaxOutputTokens, parsed.modelId)
|
|
57
|
-
?? provider.defaultMaxOutputTokens;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
55
|
function thinkingBudgetForEffort(parsed: OcxParsedRequest, reasoningEffort: string, maxOutputTokens?: number): number | undefined {
|
|
61
56
|
if (parsed.options.reasoning === "minimal") return 0;
|
|
62
57
|
const maxBudget = maxOutputTokens ?? 32768;
|
|
@@ -150,12 +145,14 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
150
145
|
body.stop = parsed.options.stopSequences;
|
|
151
146
|
}
|
|
152
147
|
const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
|
|
153
|
-
const
|
|
148
|
+
const requestedEffort = protectGlmSummaryBudget(body, provider.baseUrl, parsed.options.reasoning)
|
|
149
|
+
? "low" : parsed.options.reasoning;
|
|
150
|
+
const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, requestedEffort);
|
|
154
151
|
const explicitReasoning = applyExplicitChatReasoningWirePolicy({
|
|
155
152
|
provider,
|
|
156
153
|
modelId: parsed.modelId,
|
|
157
154
|
hasTools: !!tools,
|
|
158
|
-
requestedEffort
|
|
155
|
+
requestedEffort,
|
|
159
156
|
wireEffort: reasoningEffort,
|
|
160
157
|
reasoningDisabled,
|
|
161
158
|
body,
|
|
@@ -86,6 +86,8 @@ function buildResponseJSONWithBudget(
|
|
|
86
86
|
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
|
|
87
87
|
/** Request-visible tool names. Required for client calls when enforcement is explicitly enabled. */
|
|
88
88
|
declaredToolNames?: ReadonlySet<string>;
|
|
89
|
+
/** Bare custom declarations; unlike freeformToolNames, excludes foreign namespace children. */
|
|
90
|
+
bareCustomToolNames?: ReadonlySet<string>;
|
|
89
91
|
/** See `bridgeToResponsesSSE`: enforcement is separate from normalization (#4735). */
|
|
90
92
|
enforceDeclaredToolNames?: boolean;
|
|
91
93
|
/** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
|
|
@@ -451,7 +453,7 @@ function buildResponseJSONWithBudget(
|
|
|
451
453
|
rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
|
|
452
454
|
}
|
|
453
455
|
flushToolCall();
|
|
454
|
-
const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
|
|
456
|
+
const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames, undefined, options?.bareCustomToolNames);
|
|
455
457
|
if (
|
|
456
458
|
(options?.enforceDeclaredToolNames === true || options?.declaredToolNames != null)
|
|
457
459
|
&& options?.enforceDeclaredToolNames !== false
|
package/src/bridge/sse.ts
CHANGED
|
@@ -101,6 +101,8 @@ export function bridgeToResponsesSSE(
|
|
|
101
101
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
102
102
|
/** Request-visible tool names. Required for client calls when enforcement is explicitly enabled. */
|
|
103
103
|
declaredToolNames?: ReadonlySet<string>;
|
|
104
|
+
/** Bare custom declarations; unlike freeformToolNames, excludes foreign namespace children. */
|
|
105
|
+
bareCustomToolNames?: ReadonlySet<string>;
|
|
104
106
|
/**
|
|
105
107
|
* Whether `declaredToolNames` is an authorization boundary this proxy enforces, or only the
|
|
106
108
|
* catalog used to normalize provider-invented names back to declared ones.
|
|
@@ -1013,7 +1015,7 @@ export function bridgeToResponsesSSE(
|
|
|
1013
1015
|
rememberReasoningForCall(event.id, rawReasoningForNextToolCall, replayCacheScope);
|
|
1014
1016
|
}
|
|
1015
1017
|
if (currentToolCall) closeCurrentToolCall();
|
|
1016
|
-
const effectiveName = normalizeDeclaredToolName(event.name, options?.declaredToolNames);
|
|
1018
|
+
const effectiveName = normalizeDeclaredToolName(event.name, options?.declaredToolNames, undefined, options?.bareCustomToolNames);
|
|
1017
1019
|
const codeModeHelperName = effectiveName === "exec" && event.name !== effectiveName
|
|
1018
1020
|
? event.name
|
|
1019
1021
|
: undefined;
|
|
@@ -8,7 +8,7 @@ import type { OcxConfig } from "../types";
|
|
|
8
8
|
import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
|
|
9
9
|
import { getConfigDir } from "../config/paths";
|
|
10
10
|
import { readFileSync, existsSync } from "node:fs";
|
|
11
|
-
import { pickerCaCertPath, pickerCaFingerprints, ensurePickerCa, issuePickerLeaf, pickerLeafCertPath } from "./intercept/picker-ca";
|
|
11
|
+
import { pickerCaCertPath, pickerCaFingerprints, ensurePickerCa, issuePickerLeaf, pickerLeafCertPath, publishedPickerCaSha256 } from "./intercept/picker-ca";
|
|
12
12
|
import { inspectPickerTrust, trustPickerCa, untrustPickerCa } from "./intercept/picker-trust";
|
|
13
13
|
import type { PickerRuntime } from "./intercept/picker-runtime";
|
|
14
14
|
import type { PickerTrustState, SecurityRunner } from "./intercept/picker-trust";
|
|
@@ -36,6 +36,8 @@ export interface DesktopPickerStatus {
|
|
|
36
36
|
supported: boolean;
|
|
37
37
|
trust: PickerTrustState;
|
|
38
38
|
profile: DesktopPickerProfileInspection["kind"];
|
|
39
|
+
/** SHA-256 of this process's picker authority, so a CLI can verify the file it is about to trust; null when no authority exists yet. */
|
|
40
|
+
caSha256: string | null;
|
|
39
41
|
listenerReady: boolean;
|
|
40
42
|
effective: boolean;
|
|
41
43
|
reason: DesktopPickerReason;
|
|
@@ -107,6 +109,7 @@ function emptyStatus(config: OcxConfig, platform: NodeJS.Platform, reason: Deskt
|
|
|
107
109
|
models: 0,
|
|
108
110
|
snapshotAt: null,
|
|
109
111
|
lastBootstrapAt: null,
|
|
112
|
+
caSha256: null,
|
|
110
113
|
};
|
|
111
114
|
}
|
|
112
115
|
|
|
@@ -154,6 +157,7 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
|
|
|
154
157
|
models: runtime.models,
|
|
155
158
|
snapshotAt: runtime.snapshotAt,
|
|
156
159
|
lastBootstrapAt: runtime.lastBootstrapAt,
|
|
160
|
+
caSha256: publishedPickerCaSha256(deps.configDir),
|
|
157
161
|
};
|
|
158
162
|
}
|
|
159
163
|
|
|
@@ -231,7 +235,7 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
|
|
|
231
235
|
let trust = await inspectPickerTrust(pickerLeafCertPath(deps.configDir), caSha1, deps.security, platform);
|
|
232
236
|
observedTrust = trust;
|
|
233
237
|
if (trust !== "trusted" && options.context === "server") {
|
|
234
|
-
const added = await trustPickerCa(caPath, deps.security, platform);
|
|
238
|
+
const added = await trustPickerCa(caPath, deps.security, platform, { pem: ca.certPem });
|
|
235
239
|
trustedByAttempt = added.ok;
|
|
236
240
|
trust = await inspectPickerTrust(pickerLeafCertPath(deps.configDir), caSha1, deps.security, platform);
|
|
237
241
|
observedTrust = trust;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
readWindowsProxyBypassRegistry,
|
|
4
|
+
readWindowsSystemProxy,
|
|
5
|
+
windowsProxyOverrideBypasses,
|
|
6
|
+
type WindowsProxyBypassValues,
|
|
7
|
+
type WindowsSystemProxyResult,
|
|
8
|
+
} from "../lib/windows-system-proxy";
|
|
9
|
+
import { inspectDesktopFirstParty, observeClaudeDesktopMode } from "./desktop-first-party";
|
|
10
|
+
import { desktopFirstPartyDesired } from "./first-party-settings";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether the Windows system proxy silently bypasses Desktop first-party mode.
|
|
14
|
+
*
|
|
15
|
+
* When Claude Desktop starts the Code tab's Claude Code process, it resolves the operating
|
|
16
|
+
* system proxy for the API host and, when that yields an HTTP proxy, passes it to the process
|
|
17
|
+
* as `HTTPS_PROXY`/`HTTP_PROXY`. That inherited value takes precedence over the `env` block
|
|
18
|
+
* OpenCodex writes into the user's `~/.claude/settings.json` (only Claude Code managed settings
|
|
19
|
+
* override it), so Code-tab traffic goes to the system proxy and never reaches the intercept.
|
|
20
|
+
* Routed subagent models then fail, and nothing else reports why. A system proxy such as Clash or
|
|
21
|
+
* v2rayN, with no bypass for the API host, is the common case.
|
|
22
|
+
*
|
|
23
|
+
* Observe-only: this reads the registry and never changes it. A PAC script and WPAD automatic
|
|
24
|
+
* detection decide per request, so they are reported as undecidable rather than guessed, and a
|
|
25
|
+
* failed read is reported as unreadable rather than as an absent value. Like the other doctor
|
|
26
|
+
* proxy surfaces it never prints the proxy value, which can carry credentials.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const FIRST_PARTY_API_HOST = "api.anthropic.com";
|
|
30
|
+
|
|
31
|
+
export type DesktopSystemProxyVerdict = "no-proxy" | "bypassed" | "conflict" | "pac" | "auto-detect" | "unreadable";
|
|
32
|
+
|
|
33
|
+
export type DesktopSystemProxyAssessment =
|
|
34
|
+
| { kind: "not-applicable" }
|
|
35
|
+
| {
|
|
36
|
+
kind: DesktopSystemProxyVerdict;
|
|
37
|
+
/** settings.json carries an owned first-party env that no longer matches the port or token. */
|
|
38
|
+
settingsStale: boolean;
|
|
39
|
+
/** WPAD detection is on (or unknown) next to a static proxy; a WPAD script would win. */
|
|
40
|
+
autoDetectAlsoOn: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type DesktopFirstPartySettingsState = "applied" | "stale" | "off";
|
|
44
|
+
|
|
45
|
+
export interface DesktopSystemProxyInput {
|
|
46
|
+
platform: NodeJS.Platform;
|
|
47
|
+
firstParty: DesktopFirstPartySettingsState;
|
|
48
|
+
systemProxy: WindowsSystemProxyResult;
|
|
49
|
+
/** `null` when the Internet Settings key could not be read. */
|
|
50
|
+
bypass: WindowsProxyBypassValues | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function classify(systemProxy: WindowsSystemProxyResult, bypass: WindowsProxyBypassValues | null): DesktopSystemProxyVerdict {
|
|
54
|
+
if (bypass === null || systemProxy.kind === "unreadable") return "unreadable";
|
|
55
|
+
if (bypass.autoConfigUrl) return "pac";
|
|
56
|
+
// Desktop skips SOCKS entries, and an http=-only value does not cover an https:// API host.
|
|
57
|
+
const covering = systemProxy.kind === "proxy" && Boolean(systemProxy.httpsUrl);
|
|
58
|
+
if (!covering) return bypass.autoDetect === false ? "no-proxy" : "auto-detect";
|
|
59
|
+
// A failed WPAD lookup falls back to the static proxy, so a static conflict stands either way.
|
|
60
|
+
return windowsProxyOverrideBypasses(bypass.proxyOverride, FIRST_PARTY_API_HOST) ? "bypassed" : "conflict";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function assessDesktopSystemProxy(input: DesktopSystemProxyInput): DesktopSystemProxyAssessment {
|
|
64
|
+
if (input.platform !== "win32" || input.firstParty === "off") return { kind: "not-applicable" };
|
|
65
|
+
const kind = classify(input.systemProxy, input.bypass);
|
|
66
|
+
return {
|
|
67
|
+
kind,
|
|
68
|
+
settingsStale: input.firstParty === "stale",
|
|
69
|
+
autoDetectAlsoOn: kind === "bypassed" && input.bypass?.autoDetect !== false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const STALE_LINES = [
|
|
74
|
+
" The OpenCodex first-party env in ~/.claude/settings.json is stale (proxy port or token changed);",
|
|
75
|
+
" run `ocx ensure`, then re-run `ocx doctor`.",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function verdictLines(kind: DesktopSystemProxyVerdict): string[] {
|
|
79
|
+
switch (kind) {
|
|
80
|
+
case "no-proxy":
|
|
81
|
+
return [" ok No Windows system proxy covers the Claude API; the Code tab uses the OpenCodex proxy from settings.json."];
|
|
82
|
+
case "bypassed":
|
|
83
|
+
return [` ok ${FIRST_PARTY_API_HOST} is on the Windows proxy bypass list; the Code tab uses the OpenCodex proxy from settings.json.`];
|
|
84
|
+
case "pac":
|
|
85
|
+
return [
|
|
86
|
+
" -- Windows uses a proxy auto-config (PAC) script, so OpenCodex cannot tell whether Claude Desktop",
|
|
87
|
+
` sends ${FIRST_PARTY_API_HOST} through a proxy. If routed models fail in the Code tab, make the script return DIRECT for it.`,
|
|
88
|
+
];
|
|
89
|
+
case "auto-detect":
|
|
90
|
+
return [
|
|
91
|
+
" -- Windows automatic proxy detection (WPAD) is on or could not be read, so OpenCodex cannot tell whether",
|
|
92
|
+
` Claude Desktop sends ${FIRST_PARTY_API_HOST} through a proxy. If routed models fail in the Code tab, turn off`,
|
|
93
|
+
" \"Automatically detect settings\" or make the network's WPAD script return DIRECT for it.",
|
|
94
|
+
];
|
|
95
|
+
case "unreadable":
|
|
96
|
+
return [" -- Could not read the Windows proxy settings."];
|
|
97
|
+
case "conflict":
|
|
98
|
+
return [
|
|
99
|
+
` !! The Windows system proxy applies to ${FIRST_PARTY_API_HOST}. Claude Desktop passes it to the Code tab`,
|
|
100
|
+
" as HTTPS_PROXY, which takes precedence over the OpenCodex proxy in ~/.claude/settings.json,",
|
|
101
|
+
" so first-party routing is bypassed and routed models fail there.",
|
|
102
|
+
` Fix: add ${FIRST_PARTY_API_HOST} to your proxy client's system-proxy bypass list (Clash Verge: system_proxy_bypass),`,
|
|
103
|
+
" then fully quit and reopen Claude Desktop.",
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatDesktopSystemProxyLines(assessment: DesktopSystemProxyAssessment): string[] {
|
|
109
|
+
if (assessment.kind === "not-applicable") return [];
|
|
110
|
+
const clear = assessment.kind === "no-proxy" || assessment.kind === "bypassed";
|
|
111
|
+
// A stale env never earns an `ok`: the Code tab would reach a proxy URL that no longer matches.
|
|
112
|
+
if (clear && assessment.settingsStale) return [` -- ${STALE_LINES[0]!.trimStart()}`, STALE_LINES[1]!];
|
|
113
|
+
const lines = verdictLines(assessment.kind);
|
|
114
|
+
if (assessment.autoDetectAlsoOn) {
|
|
115
|
+
lines.push(" Automatic proxy detection (WPAD) is also on; a WPAD script on this network would decide instead.");
|
|
116
|
+
}
|
|
117
|
+
if (assessment.settingsStale) lines.push(...STALE_LINES);
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface DesktopSystemProxyDeps {
|
|
122
|
+
platform?: NodeJS.Platform;
|
|
123
|
+
firstPartyState?: () => DesktopFirstPartySettingsState;
|
|
124
|
+
readSystemProxy?: () => WindowsSystemProxyResult;
|
|
125
|
+
readBypass?: () => WindowsProxyBypassValues | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function liveFirstPartyState(
|
|
129
|
+
config: Pick<OcxConfig, "claudeCode" | "clientIntegrations" | "port" | "runtimeRole">,
|
|
130
|
+
): DesktopFirstPartySettingsState {
|
|
131
|
+
try {
|
|
132
|
+
const settings = inspectDesktopFirstParty(config).settings.kind;
|
|
133
|
+
if (settings !== "applied" && settings !== "stale") return "off";
|
|
134
|
+
return desktopFirstPartyDesired(config, observeClaudeDesktopMode(config)) ? settings : "off";
|
|
135
|
+
} catch { // no-excuse-ok: catch -- unreadable Claude settings are no first-party evidence.
|
|
136
|
+
return "off";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Reads the live state. Registry reads run only on Windows with a Desktop first-party env. */
|
|
141
|
+
export function collectDesktopSystemProxy(
|
|
142
|
+
config: Pick<OcxConfig, "claudeCode" | "clientIntegrations" | "port" | "runtimeRole">,
|
|
143
|
+
deps: DesktopSystemProxyDeps = {},
|
|
144
|
+
): DesktopSystemProxyAssessment {
|
|
145
|
+
const platform = deps.platform ?? process.platform;
|
|
146
|
+
if (platform !== "win32") return { kind: "not-applicable" };
|
|
147
|
+
const firstParty = (deps.firstPartyState ?? (() => liveFirstPartyState(config)))();
|
|
148
|
+
if (firstParty === "off") return { kind: "not-applicable" };
|
|
149
|
+
return assessDesktopSystemProxy({
|
|
150
|
+
platform,
|
|
151
|
+
firstParty,
|
|
152
|
+
systemProxy: (deps.readSystemProxy ?? readWindowsSystemProxy)(),
|
|
153
|
+
bypass: (deps.readBypass ?? readWindowsProxyBypassRegistry)(),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -96,7 +96,13 @@ export function effortFromOutputConfig(outputConfig: unknown): string | undefine
|
|
|
96
96
|
return typeof effort === "string" && OUTPUT_CONFIG_EFFORTS.has(effort) ? effort : undefined;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
function isFineTunedOpenAiTarget(model: string | undefined): boolean {
|
|
100
|
+
if (!model) return false;
|
|
101
|
+
const separator = model.lastIndexOf("/");
|
|
102
|
+
return model.slice(separator + 1).startsWith("ft:");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatFromOutputConfig(outputConfig: unknown, resolvedModel?: string): Rec | undefined {
|
|
100
106
|
if (!isRec(outputConfig) || !isRec(outputConfig.format)) return undefined;
|
|
101
107
|
const format = outputConfig.format;
|
|
102
108
|
if (
|
|
@@ -115,7 +121,9 @@ export function formatFromOutputConfig(outputConfig: unknown): Rec | undefined {
|
|
|
115
121
|
name: "response",
|
|
116
122
|
schema: format.schema,
|
|
117
123
|
// Strict Structured Outputs also needs an object at the root; a root anyOf/oneOf is refused.
|
|
118
|
-
strict: format.schema.type === "object"
|
|
124
|
+
strict: format.schema.type === "object"
|
|
125
|
+
&& !Object.hasOwn(format.schema, "anyOf") && !Object.hasOwn(format.schema, "oneOf")
|
|
126
|
+
&& satisfiesOpenAiStrictSchema(format.schema, isFineTunedOpenAiTarget(resolvedModel)),
|
|
119
127
|
};
|
|
120
128
|
}
|
|
121
129
|
|
package/src/claude/inbound.ts
CHANGED
|
@@ -460,7 +460,7 @@ function translateAnthropicRequest(
|
|
|
460
460
|
if (Array.isArray(raw.stop_sequences) && raw.stop_sequences.length > 0) {
|
|
461
461
|
body.stop = raw.stop_sequences.filter((s): s is string => typeof s === "string");
|
|
462
462
|
}
|
|
463
|
-
const outputConfigFormat = formatFromOutputConfig(raw.output_config);
|
|
463
|
+
const outputConfigFormat = formatFromOutputConfig(raw.output_config, body.model as string);
|
|
464
464
|
if (outputConfigFormat) body.text = { format: outputConfigFormat };
|
|
465
465
|
let cacheKeySource: ClaudeCacheKeySource = null;
|
|
466
466
|
if (isRec(raw.metadata) && typeof raw.metadata.user_id === "string") {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
acknowledgePendingPickerCaUntrust,
|
|
6
|
+
pendingPickerCaHasLivePublishedOwner,
|
|
7
|
+
readPendingPickerCaUntrust,
|
|
8
|
+
} from "./picker-ca";
|
|
9
|
+
import { untrustPickerCa, type SecurityRunner } from "./picker-trust";
|
|
10
|
+
|
|
11
|
+
/** Drain exactly the recorded public predecessor; keep its journal on any uncertainty. */
|
|
12
|
+
export async function drainPendingPickerCaUntrust(
|
|
13
|
+
configDir: string,
|
|
14
|
+
security?: SecurityRunner,
|
|
15
|
+
platform?: NodeJS.Platform,
|
|
16
|
+
): Promise<boolean> {
|
|
17
|
+
const pending = readPendingPickerCaUntrust(configDir);
|
|
18
|
+
if (!pending) return true;
|
|
19
|
+
if (pendingPickerCaHasLivePublishedOwner(configDir, pending)) return false;
|
|
20
|
+
|
|
21
|
+
const privateDir = mkdtempSync(join(tmpdir(), "ocx-picker-untrust-"));
|
|
22
|
+
try {
|
|
23
|
+
const publicCopy = join(privateDir, "ca.pem");
|
|
24
|
+
writeFileSync(publicCopy, pending.certPem, { mode: 0o600 });
|
|
25
|
+
const result = await untrustPickerCa(publicCopy, pending.sha1, security, platform);
|
|
26
|
+
if (!result.ok) return false;
|
|
27
|
+
return acknowledgePendingPickerCaUntrust(configDir, pending, result);
|
|
28
|
+
} finally {
|
|
29
|
+
rmSync(privateDir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|