@bitkyc08/opencodex 2.35.0 → 2.36.0-preview.20260830
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-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DPl4nBMA.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DNdRKXK9.js +0 -112
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
|
@@ -8,10 +8,12 @@ import { isDebugEnabled } from "../lib/debug-settings";
|
|
|
8
8
|
import { isCyberPolicyCode } from "../lib/errors";
|
|
9
9
|
import { redactSecretString } from "../lib/redact";
|
|
10
10
|
import { contentPartsToText } from "./image";
|
|
11
|
+
import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "./empty-tool-output-annotation";
|
|
11
12
|
import { identifyRoutedModel } from "./identity";
|
|
12
13
|
import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
|
|
13
14
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
|
|
14
15
|
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
16
|
+
import { resolveVercelGatewayRouting, vercelGatewayProviderPayload } from "../providers/vercel-gateway-routing";
|
|
15
17
|
import {
|
|
16
18
|
canForwardForeignServiceTierForChatModel,
|
|
17
19
|
fastPolicyForModel,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
} from "../providers/fastwire";
|
|
27
29
|
import { openaiChatCompletionsUrl } from "./openai-chat-url";
|
|
28
30
|
import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
|
|
31
|
+
import { agentRouterDefaultHeaders, frameAgentRouterMessages } from "./agentrouter";
|
|
29
32
|
import {
|
|
30
33
|
isXaiSchemaTarget,
|
|
31
34
|
lookupLocalJsonPointer,
|
|
@@ -87,7 +90,10 @@ function openAIChatTransport(provider: OcxProviderConfig): {
|
|
|
87
90
|
if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
|
|
88
91
|
throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
|
|
89
92
|
}
|
|
90
|
-
const headers: Record<string, string> = {
|
|
93
|
+
const headers: Record<string, string> = {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
...agentRouterDefaultHeaders(provider.baseUrl, provider.headers),
|
|
96
|
+
};
|
|
91
97
|
if (hasCredential) headers.Authorization = `Bearer ${provider.apiKey}`;
|
|
92
98
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
93
99
|
return { url: openaiChatCompletionsUrl(provider.baseUrl), headers, hasCredential };
|
|
@@ -111,7 +117,7 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
111
117
|
|
|
112
118
|
const body: Record<string, unknown> = {
|
|
113
119
|
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(modelId) : modelId,
|
|
114
|
-
messages: rawBody.messages,
|
|
120
|
+
messages: frameAgentRouterMessages(provider.baseUrl, rawBody.messages),
|
|
115
121
|
stream,
|
|
116
122
|
};
|
|
117
123
|
for (const field of CHAT_PASSTHROUGH_FIELDS) {
|
|
@@ -120,6 +126,8 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
120
126
|
|
|
121
127
|
const openRouterRouting = resolveOpenRouterRouting(provider, modelId);
|
|
122
128
|
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
|
|
129
|
+
const vercelRouting = resolveVercelGatewayRouting(provider, modelId);
|
|
130
|
+
if (vercelRouting) body.provider = vercelGatewayProviderPayload(vercelRouting);
|
|
123
131
|
|
|
124
132
|
if (modelInList(provider.noTemperatureModels, modelId)) delete body.temperature;
|
|
125
133
|
if (modelInList(provider.noTopPModels, modelId)) delete body.top_p;
|
|
@@ -588,9 +596,22 @@ function isNativeOpenAIChatTarget(provider: OcxProviderConfig): boolean {
|
|
|
588
596
|
* being flattened to the "[image]" marker the model can't actually see. Data URLs and remote https
|
|
589
597
|
* URLs are both valid in image_url.url, unlike Gemini inline_data which needs base64.
|
|
590
598
|
*/
|
|
591
|
-
function toolResultTextForWire(content: string | OcxContentPart[]): string {
|
|
592
|
-
|
|
599
|
+
function toolResultTextForWire(content: string | OcxContentPart[], annotateEmpty = false): string {
|
|
600
|
+
// An empty content array is a present-but-empty result; `contentPartsToText` would
|
|
601
|
+
// otherwise fall back to the "[image]" marker and hide the emptiness from the model.
|
|
602
|
+
if (annotateEmpty && Array.isArray(content) && content.length === 0) return EMPTY_TOOL_OUTPUT_ANNOTATION;
|
|
603
|
+
if (typeof content === "string") {
|
|
604
|
+
if (annotateEmpty && content.trim() === "") return EMPTY_TOOL_OUTPUT_ANNOTATION;
|
|
605
|
+
return content;
|
|
606
|
+
}
|
|
593
607
|
const text = content.filter((p) => p.type === "text").map((p) => (p as OcxTextContent).text).join("");
|
|
608
|
+
// A whitespace-only text-part array is the array twin of a blank string; the
|
|
609
|
+
// shared emptiness contract (same module as the Responses adapter) annotates it
|
|
610
|
+
// instead of forwarding whitespace the model silently accepts. Image parts and
|
|
611
|
+
// any other non-text part keep the array non-empty.
|
|
612
|
+
if (annotateEmpty && isWhitespaceOnlyTextPartArray(content)) {
|
|
613
|
+
return EMPTY_TOOL_OUTPUT_ANNOTATION;
|
|
614
|
+
}
|
|
594
615
|
if (text) {
|
|
595
616
|
const untransportableImages = content.filter((p) => p.type === "image" && !p.imageUrl).length;
|
|
596
617
|
return `${text}${"[image]".repeat(untransportableImages)}`;
|
|
@@ -777,7 +798,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
777
798
|
out.push({
|
|
778
799
|
role: "tool",
|
|
779
800
|
tool_call_id: toolCallId,
|
|
780
|
-
content: toolResultTextForWire(msg.content),
|
|
801
|
+
content: toolResultTextForWire(msg.content, provider.annotateEmptyToolOutputs === true),
|
|
781
802
|
});
|
|
782
803
|
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
783
804
|
pendingToolCalls.splice(matchIdx, 1);
|
|
@@ -822,7 +843,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
822
843
|
out.push({
|
|
823
844
|
role: "tool",
|
|
824
845
|
tool_call_id: toolCallId,
|
|
825
|
-
content: toolResultTextForWire(msg.content),
|
|
846
|
+
content: toolResultTextForWire(msg.content, provider.annotateEmptyToolOutputs === true),
|
|
826
847
|
});
|
|
827
848
|
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
828
849
|
flushToolResultImages();
|
|
@@ -1379,7 +1400,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1379
1400
|
|
|
1380
1401
|
buildRequest(parsed: OcxParsedRequest) {
|
|
1381
1402
|
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
1382
|
-
const messages = messagesToChatFormat(parsed, provider);
|
|
1403
|
+
const messages = frameAgentRouterMessages(provider.baseUrl, messagesToChatFormat(parsed, provider));
|
|
1383
1404
|
const tools = toolsToChatFormatForProvider(parsed, provider);
|
|
1384
1405
|
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider);
|
|
1385
1406
|
|
|
@@ -1406,6 +1427,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1406
1427
|
const maxTokens = resolveMaxTokens(provider, parsed);
|
|
1407
1428
|
const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
|
|
1408
1429
|
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
|
|
1430
|
+
const vercelRouting = resolveVercelGatewayRouting(provider, parsed.modelId);
|
|
1431
|
+
if (vercelRouting) body.provider = vercelGatewayProviderPayload(vercelRouting);
|
|
1409
1432
|
if (tools) body.tools = tools;
|
|
1410
1433
|
if (tools && toolChoice !== undefined) {
|
|
1411
1434
|
body.tool_choice = modelInList(provider.autoToolChoiceOnlyModels, parsed.modelId)
|
|
@@ -14,13 +14,14 @@ import {
|
|
|
14
14
|
isOpenAiOperatedResponsesDestination,
|
|
15
15
|
} from "../providers/openai-tiers";
|
|
16
16
|
import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
|
|
17
|
-
import { modelRecordValue } from "../reasoning-effort";
|
|
17
|
+
import { configuredReasoningEfforts, mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
18
18
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
19
19
|
import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-compat";
|
|
20
20
|
import { rewriteRoutedToolSearchForUpstream } from "../responses/tool-search-compat";
|
|
21
21
|
import { rewriteRoutedNamespaceToolsForUpstream } from "../responses/namespace-tool-compat";
|
|
22
22
|
import { openaiResponsesUrl } from "./openai-responses-url";
|
|
23
|
-
import { normalizeXaiResponsesWebSearch } from "./xai-web-search";
|
|
23
|
+
import { injectXaiResponsesXSearch, normalizeXaiResponsesWebSearch } from "./xai-web-search";
|
|
24
|
+
import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "./empty-tool-output-annotation";
|
|
24
25
|
import {
|
|
25
26
|
isXaiSchemaTarget,
|
|
26
27
|
normalizeXaiToolParameters,
|
|
@@ -549,6 +550,27 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
|
549
550
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
550
551
|
}
|
|
551
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Apply the routed provider's real effort ladder to an existing Responses reasoning field.
|
|
555
|
+
* Native forward requests keep the server-owned native clamp; unknown third-party ladders stay
|
|
556
|
+
* byte-equivalent instead of acquiring a policy from this adapter.
|
|
557
|
+
*/
|
|
558
|
+
function mapRoutedResponsesReasoningEffort(
|
|
559
|
+
body: unknown,
|
|
560
|
+
provider: OcxProviderConfig,
|
|
561
|
+
modelId: string,
|
|
562
|
+
): unknown {
|
|
563
|
+
if (provider.authMode === "forward") return body;
|
|
564
|
+
if (configuredReasoningEfforts(provider, modelId) === undefined) return body;
|
|
565
|
+
if (!isPlainObject(body) || !isPlainObject(body.reasoning)) return body;
|
|
566
|
+
const requested = body.reasoning.effort;
|
|
567
|
+
if (typeof requested !== "string") return body;
|
|
568
|
+
|
|
569
|
+
const mapped = mapReasoningEffort(provider, modelId, requested);
|
|
570
|
+
if (!mapped || mapped === requested) return body;
|
|
571
|
+
return { ...body, reasoning: { ...body.reasoning, effort: mapped } };
|
|
572
|
+
}
|
|
573
|
+
|
|
552
574
|
function normalizeFunctionToolSchema(tool: unknown, xaiTarget: boolean): unknown | undefined {
|
|
553
575
|
if (!isPlainObject(tool) || tool.type !== "function") return tool;
|
|
554
576
|
if (xaiTarget) {
|
|
@@ -821,6 +843,40 @@ function toolOutputText(output: unknown): string {
|
|
|
821
843
|
}).filter(Boolean).join("\n");
|
|
822
844
|
}
|
|
823
845
|
|
|
846
|
+
/** True when a Responses tool output item is present but carries no usable content. */
|
|
847
|
+
function isToolOutputEmpty(output: unknown): boolean {
|
|
848
|
+
if (typeof output === "string") return output.trim() === "";
|
|
849
|
+
if (Array.isArray(output)) {
|
|
850
|
+
// Mirror the Chat wire rule through the shared contract: only a pure
|
|
851
|
+
// text/refusal part array whose joined content trims empty is annotated.
|
|
852
|
+
// input_image, encrypted_content, input_file and any other non-text part is
|
|
853
|
+
// real output and must never be replaced.
|
|
854
|
+
return isWhitespaceOnlyTextPartArray(output);
|
|
855
|
+
}
|
|
856
|
+
// A missing or null `output` is not a present-but-empty result: it is an
|
|
857
|
+
// incomplete payload. Leave it untouched so the upstream contract fails
|
|
858
|
+
// closed, and the orphan repair can surface it honestly instead of claiming
|
|
859
|
+
// the tool ran with no output.
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Rewrite present-but-empty tool outputs to an explicit annotation. Synthetic
|
|
865
|
+
* missing-result placeholders are non-empty and pass through untouched. No-op unless
|
|
866
|
+
* the provider opts in (`annotateEmptyToolOutputs`).
|
|
867
|
+
*/
|
|
868
|
+
function annotateEmptyResponsesToolOutputs(body: unknown, enabled: boolean): unknown {
|
|
869
|
+
if (!enabled || !isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
870
|
+
let changed = false;
|
|
871
|
+
const input = body.input.map(item => {
|
|
872
|
+
if (!isPlainObject(item) || (item.type !== "function_call_output" && item.type !== "custom_tool_call_output")) return item;
|
|
873
|
+
if (!isToolOutputEmpty(item.output)) return item;
|
|
874
|
+
changed = true;
|
|
875
|
+
return { ...item, output: EMPTY_TOOL_OUTPUT_ANNOTATION };
|
|
876
|
+
});
|
|
877
|
+
return changed ? { ...body, input } : body;
|
|
878
|
+
}
|
|
879
|
+
|
|
824
880
|
/**
|
|
825
881
|
* Repair a forward-mode input array whose continuation context was lost. When the replay
|
|
826
882
|
* expansion misses (proxy restart, unrecorded prior turn), previous_response_id is stripped
|
|
@@ -1202,6 +1258,13 @@ function canonicalForwardSystemText(item: Record<string, unknown>): string | nul
|
|
|
1202
1258
|
return text;
|
|
1203
1259
|
}
|
|
1204
1260
|
|
|
1261
|
+
/** Only message items may carry privileged system instructions. */
|
|
1262
|
+
function isCanonicalForwardSystemMessage(item: unknown): item is Record<string, unknown> {
|
|
1263
|
+
return isPlainObject(item)
|
|
1264
|
+
&& (item.type === undefined || item.type === "message")
|
|
1265
|
+
&& item.role === "system";
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1205
1268
|
/**
|
|
1206
1269
|
* The public Responses API accepts input system messages and `truncation`, but the canonical
|
|
1207
1270
|
* ChatGPT Codex forward endpoint rejects both. Fold only fully textual system messages into the
|
|
@@ -1225,7 +1288,7 @@ function normalizeCanonicalForwardPromptEnvelope(body: unknown): unknown {
|
|
|
1225
1288
|
let sawSystemMessage = false;
|
|
1226
1289
|
let canFoldAllSystemMessages = true;
|
|
1227
1290
|
for (const item of input) {
|
|
1228
|
-
if (!
|
|
1291
|
+
if (!isCanonicalForwardSystemMessage(item)) continue;
|
|
1229
1292
|
sawSystemMessage = true;
|
|
1230
1293
|
const text = canonicalForwardSystemText(item);
|
|
1231
1294
|
if (text === null) {
|
|
@@ -1239,7 +1302,7 @@ function normalizeCanonicalForwardPromptEnvelope(body: unknown): unknown {
|
|
|
1239
1302
|
const next: Record<string, unknown> = { ...body };
|
|
1240
1303
|
if (stripTruncation) delete next.truncation;
|
|
1241
1304
|
if (sawSystemMessage && canFoldAllSystemMessages) {
|
|
1242
|
-
next.input = input.filter(item => !
|
|
1305
|
+
next.input = input.filter(item => !isCanonicalForwardSystemMessage(item));
|
|
1243
1306
|
const folded = foldedText.join("\n\n");
|
|
1244
1307
|
if (folded !== "") {
|
|
1245
1308
|
const existing = typeof body.instructions === "string" ? body.instructions : "";
|
|
@@ -1991,6 +2054,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1991
2054
|
parsed._rawBody,
|
|
1992
2055
|
forward || parsed._previousResponseInputExpanded === true,
|
|
1993
2056
|
);
|
|
2057
|
+
outBody = mapRoutedResponsesReasoningEffort(outBody, provider, parsed.modelId);
|
|
1994
2058
|
// stripPreviousResponseId() intentionally returns its input on a no-op. Detach before the
|
|
1995
2059
|
// tier write so a force-fast/default decision can never mutate parsed._rawBody.
|
|
1996
2060
|
outBody = applyTierDecisionToResponsesBody(outBody, parsed.options?.tierDecision);
|
|
@@ -2001,6 +2065,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2001
2065
|
// pair from its own storage either, so it needs the same repair the forward
|
|
2002
2066
|
// backend gets — dropping previous_response_id is not much use if the body that
|
|
2003
2067
|
// reaches the wire is unparseable.
|
|
2068
|
+
if (provider.annotateEmptyToolOutputs === true) {
|
|
2069
|
+
outBody = annotateEmptyResponsesToolOutputs(outBody, true);
|
|
2070
|
+
}
|
|
2004
2071
|
if (forward || stateless) {
|
|
2005
2072
|
outBody = repairOrphanedInputItems(outBody, unexpandedMiss, stateless && !forward);
|
|
2006
2073
|
}
|
|
@@ -2063,6 +2130,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2063
2130
|
// Preserve xAI's cached-only fail-closed semantics and image-search mapping before the
|
|
2064
2131
|
// generic capability fallback removes the private OpenAI fields.
|
|
2065
2132
|
outBody = normalizeXaiResponsesWebSearch(outBody, provider);
|
|
2133
|
+
outBody = injectXaiResponsesXSearch(outBody, provider, parsed._replayPrefixLen);
|
|
2066
2134
|
// xAI and explicitly classified compatible gateways reject these OpenAI web_search
|
|
2067
2135
|
// extensions. Keep them for OpenAI API-key traffic and unclassified gateways.
|
|
2068
2136
|
if (provider.supportsOpenAiWebSearchToolFields === false) {
|
package/src/adapters/registry.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { createGoogleAdapter } from "./google";
|
|
|
8
8
|
import { createKiroAdapter } from "./kiro";
|
|
9
9
|
import { createMimoFreeAdapter } from "./mimo-free";
|
|
10
10
|
import { createOpenAIChatAdapter } from "./openai-chat";
|
|
11
|
+
import { createOllamaNativeAdapter } from "./ollama-native";
|
|
11
12
|
import { createResponsesPassthroughAdapter } from "./openai-responses";
|
|
12
13
|
import type { OcxProviderConfig } from "../types";
|
|
13
14
|
import { createAdapterTierMetadata } from "../providers/fastwire";
|
|
@@ -21,6 +22,7 @@ export interface AdapterFactoryContext {
|
|
|
21
22
|
export type AdapterWire =
|
|
22
23
|
| "command-code"
|
|
23
24
|
| "openai-chat"
|
|
25
|
+
| "ollama-native"
|
|
24
26
|
| "anthropic"
|
|
25
27
|
| "openai-responses"
|
|
26
28
|
| "google"
|
|
@@ -62,6 +64,11 @@ export const ADAPTER_REGISTRY = {
|
|
|
62
64
|
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) =>
|
|
63
65
|
withClinePassDeepSeekV4ToolReplayCompatibility(createOpenAIChatAdapter(provider)),
|
|
64
66
|
},
|
|
67
|
+
"ollama-native": {
|
|
68
|
+
wire: "ollama-native",
|
|
69
|
+
mutation: "codex-owned",
|
|
70
|
+
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createOllamaNativeAdapter(provider),
|
|
71
|
+
},
|
|
65
72
|
anthropic: {
|
|
66
73
|
wire: "anthropic",
|
|
67
74
|
mutation: "codex-owned",
|
|
@@ -3,6 +3,7 @@ import { isXaiResponsesDestination } from "../providers/xai-transport";
|
|
|
3
3
|
|
|
4
4
|
const CODEX_WEB_SEARCH_TOOL = "web_search";
|
|
5
5
|
const CODEX_WEB_SEARCH_PREVIEW_TOOL = "web_search_preview";
|
|
6
|
+
const XAI_SEARCH_TOOL = "x_search";
|
|
6
7
|
|
|
7
8
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
8
9
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -135,6 +136,15 @@ function normalizeToolChoice(body: Record<string, unknown>): Record<string, unkn
|
|
|
135
136
|
return body;
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
function currentInputStart(inputLength: number, replayPrefixLength: number | undefined): number {
|
|
140
|
+
if (typeof replayPrefixLength !== "number" || !Number.isFinite(replayPrefixLength)) return 0;
|
|
141
|
+
return Math.min(inputLength, Math.max(0, Math.trunc(replayPrefixLength)));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hasToolType(tools: unknown, type: string): boolean {
|
|
145
|
+
return Array.isArray(tools) && tools.some(tool => isPlainObject(tool) && tool.type === type);
|
|
146
|
+
}
|
|
147
|
+
|
|
138
148
|
/**
|
|
139
149
|
* Make Codex's hosted web-search declaration acceptable to xAI Responses without changing other
|
|
140
150
|
* providers or mutating the caller-owned request body.
|
|
@@ -184,3 +194,51 @@ export function normalizeXaiResponsesWebSearch(
|
|
|
184
194
|
|
|
185
195
|
return normalizeToolChoice(next);
|
|
186
196
|
}
|
|
197
|
+
|
|
198
|
+
function isLiveWebSearchTool(tool: unknown): boolean {
|
|
199
|
+
return isPlainObject(tool)
|
|
200
|
+
&& tool.type === CODEX_WEB_SEARCH_TOOL
|
|
201
|
+
&& (!Object.hasOwn(tool, "external_web_access") || tool.external_web_access === true);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Add xAI's hosted X search declaration without changing web-search normalization or selectors.
|
|
206
|
+
* Destination classification belongs only to this opt-in injection path; the public-API
|
|
207
|
+
* normalizer above intentionally retains its narrower causality boundary.
|
|
208
|
+
*/
|
|
209
|
+
export function injectXaiResponsesXSearch(
|
|
210
|
+
body: unknown,
|
|
211
|
+
provider: Pick<OcxProviderConfig, "baseUrl" | "xaiResponsesXSearch">,
|
|
212
|
+
replayPrefixLength?: number,
|
|
213
|
+
): unknown {
|
|
214
|
+
if (
|
|
215
|
+
!isPlainObject(body)
|
|
216
|
+
|| !isXaiResponsesDestination(provider)
|
|
217
|
+
|| provider.xaiResponsesXSearch !== true
|
|
218
|
+
) return body;
|
|
219
|
+
|
|
220
|
+
const input = Array.isArray(body.input) ? body.input : undefined;
|
|
221
|
+
const inputStart = input ? currentInputStart(input.length, replayPrefixLength) : 0;
|
|
222
|
+
const currentInput = input?.slice(inputStart) ?? [];
|
|
223
|
+
const currentXSearchDeclared = hasToolType(body.tools, XAI_SEARCH_TOOL)
|
|
224
|
+
|| currentInput.some(item =>
|
|
225
|
+
isPlainObject(item)
|
|
226
|
+
&& item.type === "additional_tools"
|
|
227
|
+
&& hasToolType(item.tools, XAI_SEARCH_TOOL)
|
|
228
|
+
);
|
|
229
|
+
if (currentXSearchDeclared) return body;
|
|
230
|
+
|
|
231
|
+
const liveWebSearchSurvives = Array.isArray(body.tools) && body.tools.some(isLiveWebSearchTool)
|
|
232
|
+
|| currentInput.some(item =>
|
|
233
|
+
isPlainObject(item)
|
|
234
|
+
&& item.type === "additional_tools"
|
|
235
|
+
&& Array.isArray(item.tools)
|
|
236
|
+
&& item.tools.some(isLiveWebSearchTool)
|
|
237
|
+
);
|
|
238
|
+
if (!liveWebSearchSurvives) return body;
|
|
239
|
+
|
|
240
|
+
// Declaration does not grant selection when `tool_choice` names a specific tool or carries an
|
|
241
|
+
// `allowed_tools` set that excludes x_search, so leave that selector byte-shape untouched.
|
|
242
|
+
const tools = Array.isArray(body.tools) ? body.tools : [];
|
|
243
|
+
return { ...body, tools: [...tools, { type: XAI_SEARCH_TOOL }] };
|
|
244
|
+
}
|
package/src/claude/desktop-3p.ts
CHANGED
|
@@ -585,7 +585,9 @@ export function writeDesktop3pConfig(
|
|
|
585
585
|
? metadata.entries.map(current => current === existing ? entry : current)
|
|
586
586
|
: [...metadata.entries, entry];
|
|
587
587
|
|
|
588
|
-
const
|
|
588
|
+
const generated = generateDesktop3pConfig(port, nativeSlugs, routedModels, apiKey, mode, profile, nativeContextCap);
|
|
589
|
+
const preserved = readDesktopProfileForeignKeys(configPath);
|
|
590
|
+
const configJson = JSON.stringify({ ...preserved, ...generated }, null, 2) + "\n";
|
|
589
591
|
const fingerprint = createHash("sha256").update(configJson).digest("hex").slice(0, 16);
|
|
590
592
|
const { backupPath } = atomicReplaceDesktopConfig(configPath, configJson);
|
|
591
593
|
try {
|
|
@@ -602,6 +604,24 @@ export function writeDesktop3pConfig(
|
|
|
602
604
|
}
|
|
603
605
|
}
|
|
604
606
|
|
|
607
|
+
const OPENCODEX_DESKTOP_PROFILE_KEYS = new Set([
|
|
608
|
+
"inferenceProvider",
|
|
609
|
+
"inferenceCredentialKind",
|
|
610
|
+
"inferenceGatewayBaseUrl",
|
|
611
|
+
"inferenceGatewayApiKey",
|
|
612
|
+
"modelDiscoveryEnabled",
|
|
613
|
+
"inferenceModels",
|
|
614
|
+
]);
|
|
615
|
+
|
|
616
|
+
function readDesktopProfileForeignKeys(path: string): Record<string, unknown> {
|
|
617
|
+
if (!existsSync(path)) return {};
|
|
618
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
619
|
+
if (!isRecord(parsed)) throw new Error("Claude Desktop 3P profile is not a JSON object");
|
|
620
|
+
return Object.fromEntries(
|
|
621
|
+
Object.entries(parsed).filter(([key]) => !OPENCODEX_DESKTOP_PROFILE_KEYS.has(key)),
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
605
625
|
/** Backup an existing owned config then atomically replace it. Exported for failure-path tests. */
|
|
606
626
|
export function atomicReplaceDesktopConfig(
|
|
607
627
|
path: string,
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Read-only, privacy-safe Windows policy diagnosis for Claude Desktop 3P. */
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { win32 } from "node:path";
|
|
4
|
+
import { resolveTrustedWindowsSystemDirectory } from "../lib/windows-elevation";
|
|
5
|
+
import { decodeWindowsTextBytes } from "../lib/windows-text";
|
|
6
|
+
|
|
7
|
+
const CLAUDE_POLICY_KEY = "HKLM\\SOFTWARE\\Policies\\Claude";
|
|
8
|
+
const CLAUDE_POLICY_PARENT_KEY = "HKLM\\SOFTWARE\\Policies";
|
|
9
|
+
const POLICY_PROBE_TIMEOUT_MS = 2_000;
|
|
10
|
+
|
|
11
|
+
export type ClaudeDesktopPolicyState = "present" | "absent" | "unknown" | "not_applicable";
|
|
12
|
+
|
|
13
|
+
export interface ClaudeDesktopPolicyProbeResult {
|
|
14
|
+
readonly status: number | null;
|
|
15
|
+
/** Kept inside the probe boundary; diagnostics never return or log it. */
|
|
16
|
+
readonly stdout: string;
|
|
17
|
+
readonly timedOut: boolean;
|
|
18
|
+
readonly spawnFailed: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ClaudeDesktopPolicyProbeRunner = (
|
|
22
|
+
file: string,
|
|
23
|
+
args: readonly string[],
|
|
24
|
+
) => ClaudeDesktopPolicyProbeResult;
|
|
25
|
+
|
|
26
|
+
export interface ClaudeDesktopPolicyProbeOptions {
|
|
27
|
+
readonly platform?: NodeJS.Platform;
|
|
28
|
+
readonly run?: ClaudeDesktopPolicyProbeRunner;
|
|
29
|
+
readonly resolveSystemDirectory?: () => string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ClaudeDesktopPolicyHealth {
|
|
33
|
+
readonly ok: boolean;
|
|
34
|
+
readonly status: "ok" | "warning";
|
|
35
|
+
readonly state: ClaudeDesktopPolicyState;
|
|
36
|
+
readonly message: string;
|
|
37
|
+
readonly action: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const defaultPolicyProbeRunner: ClaudeDesktopPolicyProbeRunner = (file, args) => {
|
|
41
|
+
const result = spawnSync(file, [...args], {
|
|
42
|
+
encoding: "buffer",
|
|
43
|
+
maxBuffer: 64 * 1024,
|
|
44
|
+
timeout: POLICY_PROBE_TIMEOUT_MS,
|
|
45
|
+
windowsHide: true,
|
|
46
|
+
});
|
|
47
|
+
const errorCode = (result.error as NodeJS.ErrnoException | undefined)?.code;
|
|
48
|
+
return {
|
|
49
|
+
status: result.status,
|
|
50
|
+
// Decoded only to prove key absence. This never crosses the probe boundary.
|
|
51
|
+
stdout: result.stdout ? decodeWindowsTextBytes(result.stdout) : "",
|
|
52
|
+
timedOut: errorCode === "ETIMEDOUT" || result.signal !== null,
|
|
53
|
+
spawnFailed: result.error !== undefined && errorCode !== "ETIMEDOUT",
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function usable(result: ClaudeDesktopPolicyProbeResult): boolean {
|
|
58
|
+
return !result.timedOut && !result.spawnFailed && result.status !== null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parentListsPolicyKey(output: string): boolean {
|
|
62
|
+
const expected = CLAUDE_POLICY_KEY.toLowerCase();
|
|
63
|
+
return output.split(/\r?\n/).some(line => line.trim().toLowerCase()
|
|
64
|
+
.replace(/^hkey_local_machine\\/, "hklm\\") === expected);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Detect machine-level Claude managed policy without reading or exposing its contents.
|
|
69
|
+
*
|
|
70
|
+
* `reg.exe` returns exit 1 for both a missing key and query/access failures. As in the
|
|
71
|
+
* Windows tray registry reader, absence is therefore accepted only when the immediate
|
|
72
|
+
* parent can be queried successfully. Every other failure stays unknown.
|
|
73
|
+
*/
|
|
74
|
+
export function probeClaudeDesktopPolicy(
|
|
75
|
+
options: ClaudeDesktopPolicyProbeOptions = {},
|
|
76
|
+
): ClaudeDesktopPolicyState {
|
|
77
|
+
const platform = options.platform ?? process.platform;
|
|
78
|
+
if (platform !== "win32") return "not_applicable";
|
|
79
|
+
|
|
80
|
+
let regExe: string;
|
|
81
|
+
try {
|
|
82
|
+
const systemDirectory = (options.resolveSystemDirectory ?? resolveTrustedWindowsSystemDirectory)();
|
|
83
|
+
regExe = win32.join(systemDirectory, "reg.exe");
|
|
84
|
+
} catch {
|
|
85
|
+
return "unknown";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const run = options.run ?? defaultPolicyProbeRunner;
|
|
89
|
+
let policy: ClaudeDesktopPolicyProbeResult;
|
|
90
|
+
try {
|
|
91
|
+
policy = run(regExe, ["query", CLAUDE_POLICY_KEY, "/reg:64"]);
|
|
92
|
+
} catch {
|
|
93
|
+
return "unknown";
|
|
94
|
+
}
|
|
95
|
+
if (!usable(policy)) return "unknown";
|
|
96
|
+
if (policy.status === 0) return "present";
|
|
97
|
+
if (policy.status !== 1) return "unknown";
|
|
98
|
+
|
|
99
|
+
let parent: ClaudeDesktopPolicyProbeResult;
|
|
100
|
+
try {
|
|
101
|
+
parent = run(regExe, ["query", CLAUDE_POLICY_PARENT_KEY, "/reg:64"]);
|
|
102
|
+
} catch {
|
|
103
|
+
return "unknown";
|
|
104
|
+
}
|
|
105
|
+
if (!usable(parent) || parent.status !== 0) return "unknown";
|
|
106
|
+
// The child can be listed by a readable parent while its own ACL blocks the
|
|
107
|
+
// query. That is unreadable, not absent.
|
|
108
|
+
return parentListsPolicyKey(parent.stdout) ? "unknown" : "absent";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** State-only health projection shared by CLI, apply, and management status. */
|
|
112
|
+
export function claudeDesktopPolicyHealth(
|
|
113
|
+
state: ClaudeDesktopPolicyState,
|
|
114
|
+
): ClaudeDesktopPolicyHealth {
|
|
115
|
+
if (state === "present") {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
status: "warning",
|
|
119
|
+
state,
|
|
120
|
+
message: "Windows managed Claude policy is active, so Claude Desktop will ignore the local third-party profile.",
|
|
121
|
+
action: "Ask your administrator to remove or revise the managed Claude policy, then fully quit and reopen Claude Desktop.",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (state === "unknown") {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
status: "warning",
|
|
128
|
+
state,
|
|
129
|
+
message: "OpenCodex could not verify Windows managed Claude policy; Desktop third-party profile health is unverified.",
|
|
130
|
+
action: "Check access to Windows machine policy and run the status check again before relying on Claude Desktop 3P.",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
ok: true,
|
|
135
|
+
status: "ok",
|
|
136
|
+
state,
|
|
137
|
+
message: state === "absent"
|
|
138
|
+
? "No Windows managed Claude policy was detected."
|
|
139
|
+
: "Windows managed Claude policy is not applicable on this platform.",
|
|
140
|
+
action: "No action required.",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function claudeDesktopPolicyWarning(
|
|
145
|
+
state: ClaudeDesktopPolicyState,
|
|
146
|
+
): string | undefined {
|
|
147
|
+
const health = claudeDesktopPolicyHealth(state);
|
|
148
|
+
return health.ok ? undefined : `${health.message} Action: ${health.action}`;
|
|
149
|
+
}
|
package/src/cli/account.ts
CHANGED
|
@@ -25,8 +25,19 @@ type TargetProvenance = "live-oauth-list" | "config" | "codex";
|
|
|
25
25
|
|
|
26
26
|
const MAIN_ALIAS = "main";
|
|
27
27
|
const MAIN_CODEX_ID = "__main__";
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Replacement-style single-slot OAuth (no stable identity; not HTTP-derivable).
|
|
30
|
+
*
|
|
31
|
+
* Empty since `d82b3049d` gave Kiro a quota-aware account pool: multiple Kiro accounts are
|
|
32
|
+
* stored under multiauth, ranked by remaining headroom in `rankAccountsByHeadroom`, and
|
|
33
|
+
* rotated on 429 by the generic OAuth failover path, which does not exclude Kiro. Printing a
|
|
34
|
+
* "single login slot" note alongside a list of several pooled accounts told operators the
|
|
35
|
+
* opposite of what the runtime does.
|
|
36
|
+
*
|
|
37
|
+
* Kept as a named seam rather than deleted: the replacement-style shape is a real category,
|
|
38
|
+
* and a future provider without stable per-account identity belongs here.
|
|
39
|
+
*/
|
|
40
|
+
const REPLACEMENT_STYLE_OAUTH = new Set<string>();
|
|
30
41
|
|
|
31
42
|
const ACCOUNT_USAGE = `Usage:
|
|
32
43
|
ocx account list [provider] [--json] [--all] [--quota [--refresh]]
|
|
@@ -111,6 +122,9 @@ function quotaText(row: AccountRow): string {
|
|
|
111
122
|
const short = quota.fiveHourPercent ?? quota.shortPercent;
|
|
112
123
|
if (typeof short === "number") parts.push(`5h ${short}%`);
|
|
113
124
|
if (typeof quota.weeklyPercent === "number") parts.push(`wk ${quota.weeklyPercent}%`);
|
|
125
|
+
// Kiro bills a monthly allowance and reports no shorter window, so without this arm a
|
|
126
|
+
// perfectly healthy Kiro account prints "-" and reads as broken.
|
|
127
|
+
if (typeof quota.monthlyPercent === "number") parts.push(`mo ${Math.round(quota.monthlyPercent)}%`);
|
|
114
128
|
return parts.length > 0 ? parts.join(" ") : "-";
|
|
115
129
|
}
|
|
116
130
|
|
|
@@ -37,7 +37,8 @@ export interface ApplyProfileDeps {
|
|
|
37
37
|
postApplyImpl?: (
|
|
38
38
|
mode: Desktop3pConfigMode,
|
|
39
39
|
profile: DesktopProfile,
|
|
40
|
-
) => Promise<{ ok?: boolean; path?: string; error?: string }>;
|
|
40
|
+
) => Promise<{ ok?: boolean; path?: string; error?: string; warning?: string }>;
|
|
41
|
+
probeClaudeDesktopPolicy?: typeof import("../claude/desktop-policy").probeClaudeDesktopPolicy;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export async function applyProfile(
|
|
@@ -71,10 +72,11 @@ export async function applyProfile(
|
|
|
71
72
|
// Partial success: Desktop was written but the applied marker was not
|
|
72
73
|
// persisted. Pass the degradation up instead of reporting a clean apply.
|
|
73
74
|
const partial = (applied as { saved?: boolean; warning?: string }).saved === false;
|
|
75
|
+
const warning = (applied as { warning?: string }).warning;
|
|
74
76
|
return {
|
|
75
77
|
ok: true,
|
|
76
78
|
path: applied.path ?? "",
|
|
77
|
-
...(
|
|
79
|
+
...(warning ? { warning } : partial ? { warning: "applied marker was not saved" } : {}),
|
|
78
80
|
};
|
|
79
81
|
} catch (error) {
|
|
80
82
|
return { ok: false, path: "", reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -102,7 +104,15 @@ export async function applyProfile(
|
|
|
102
104
|
state.profile,
|
|
103
105
|
nativeContextLimits(config),
|
|
104
106
|
);
|
|
105
|
-
|
|
107
|
+
const { claudeDesktopPolicyWarning, probeClaudeDesktopPolicy } = await import("../claude/desktop-policy");
|
|
108
|
+
const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)();
|
|
109
|
+
const warning = result.written ? claudeDesktopPolicyWarning(policyState) : undefined;
|
|
110
|
+
return {
|
|
111
|
+
ok: result.written,
|
|
112
|
+
path: result.path,
|
|
113
|
+
reason: result.reason,
|
|
114
|
+
...(warning ? { warning } : {}),
|
|
115
|
+
};
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProfileDeps = {}): Promise<number> {
|
package/src/cli/combo.ts
CHANGED
|
@@ -14,7 +14,7 @@ const USAGE = `Usage:
|
|
|
14
14
|
ocx combo [list] [--json]
|
|
15
15
|
ocx combo show <id> [--json]
|
|
16
16
|
ocx combo set <id> --targets <provider/model[:weight],...>
|
|
17
|
-
[--strategy <failover|round-robin>] [--sticky <1-100>]
|
|
17
|
+
[--strategy <failover|round-robin|random|least-used|reset-window>] [--sticky <1-100>]
|
|
18
18
|
[--effort <low|medium|high|xhigh|max|ultra|->] [--alias <name|->]
|
|
19
19
|
[--native-alias] [--display-name <label|->]
|
|
20
20
|
[--rename-from <id>] [--json]
|
|
@@ -73,9 +73,12 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
73
73
|
const targetsRaw = takeOption(args, "--targets");
|
|
74
74
|
if (!targetsRaw) throw new CliUsageError("--targets is required", USAGE);
|
|
75
75
|
const strategy = takeOption(args, "--strategy") ?? "failover";
|
|
76
|
-
if (strategy !== "failover" && strategy !== "round-robin") throw new CliUsageError("--strategy must be failover or
|
|
77
|
-
const stickyLimit = takeIntegerOption(args, "--sticky", { min: 1 })
|
|
78
|
-
if (stickyLimit
|
|
76
|
+
if (strategy !== "failover" && strategy !== "round-robin" && strategy !== "random" && strategy !== "least-used" && strategy !== "reset-window") throw new CliUsageError("--strategy must be failover, round-robin, random, least-used, or reset-window", USAGE);
|
|
77
|
+
const stickyLimit = takeIntegerOption(args, "--sticky", { min: 1 });
|
|
78
|
+
if (stickyLimit !== undefined) {
|
|
79
|
+
if (stickyLimit > 100) throw new CliUsageError("--sticky must be <= 100", USAGE);
|
|
80
|
+
if (strategy !== "round-robin") throw new CliUsageError("--sticky applies only to round-robin", USAGE);
|
|
81
|
+
}
|
|
79
82
|
const effort = takeOption(args, "--effort");
|
|
80
83
|
const alias = takeOption(args, "--alias");
|
|
81
84
|
const nativeAlias = takeFlag(args, "--native-alias");
|
|
@@ -84,7 +87,7 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
84
87
|
rejectArgs(args, USAGE);
|
|
85
88
|
const combo: Record<string, unknown> = {
|
|
86
89
|
strategy,
|
|
87
|
-
stickyLimit,
|
|
90
|
+
stickyLimit: stickyLimit ?? 1,
|
|
88
91
|
targets: parseTargets(targetsRaw),
|
|
89
92
|
};
|
|
90
93
|
if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort;
|